Vue2响应式原理解析和实现

核心原理

Vue通过 Object.defineProperty 的 getter/setter 对收集的依赖项进行监听,在属性被访问和修改时通知变化,进而更新视图数据。

Vue2响应式原理解析和实现_第1张图片

  • 监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
  • 订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理;
  • 订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;

Object.defineProperty

Object.defineProperty

**Object.defineProperty()** 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, prop, descriptor)
  • obj

    要定义属性的对象。

  • prop

    要定义或修改的属性的名称或 Symbol

  • descriptor

    要定义或修改的属性描述符。

// 响应式函数
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`访问了${key}属性`)
            return val
        },
        set(newValue) {
            console.log(`${key}由->${val}->设置成->${newValue}`)
            if (val == newValue) {
                return;
            }
            val = newValue
        }
    })
}


const data = {
    name: 'Tom',
    age: 3
}

const observe = function(data) {
    for (let key in data) {
        defineReactive(data, key, data[key])
    };
}

observe(data);

console.log(data.name)
// 访问了name属性
// Tom
data.name = 'Jerry' // 将name由->Tom->设置成->Jerry
console.log(data.name)
  // 访问了name属性
  // Jerry

实现一个简化版的响应式

Vue 初始化实例

const Vue = function(options) {
  const self = this;
  // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
  if (options && typeof options.data === 'function') {
    this._data = options.data.apply(this);
  }
  // 挂载函数
  this.mount = function() {
    new Watcher(self, self.render);
  }
  // 渲染函数
  this.render = function() {
      console.log(self._data.text);
  }
  // 监听this._data
  observe(this._data);  
}

const vue = new Vue({
  data() {
    return {
      text: 'hello world'
    };
  }
})

vue.mount(); // in get /n hello world
vue._data.text = '123'; // in watcher update /n in get /n 123

Observer

const observe = function(data) {
  return new Observer(data);
}

const Observer = function(data) {
    // 循环修改为每个属性添加get set
    for (let key in data) {
        defineReactive(data, key, data[key]);
    }
}

const defineReactive = function(obj, key, val) {
    // 局部变量dep,用于get set内部调用
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        // 设置当前描述属性为可被循环
        enumerable: true,
        // 设置当前描述属性可被修改
        configurable: true,
        get() {
            console.log('in get');
            // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
            dep.depend();
            return val;
        },
        set(newVal) {
            if (newVal === val) {
                return;
            }
            val = newVal;
            // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
            // 这里每个需要更新通过什么断定?dep.subs
            dep.notify();
        }
    });
}

Vue 初始化时,进行了数据的 get、set 绑定,并创建了一个 Dep 对象。

对于数据的 get、set 绑定我们并不陌生,但是 Dep 对象什么呢?

Dep 对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅,我们一起来剖析一下。

Dep

const Dep = function() {
  const self = this;
  // 收集目标
  this.target = null;
  // 存储收集器中需要通知的Watcher
  this.subs = [];
  // 当有目标时,绑定Dep与Wathcer的关系
  this.depend = function() {
    if (Dep.target) {
      // 这里其实可以直接写self.addSub(Dep.target),
      // 没有这么写因为想还原源码的过程。
      Dep.target.addDep(self);
    }
  }
  // 为当前收集器添加Watcher
  this.addSub = function(watcher) {
    self.subs.push(watcher);
  }
  // 通知收集器中所的所有Wathcer,调用其update方法
  this.notify = function() {
    for (let i = 0; i < self.subs.length; i += 1) {
      self.subs[i].update();
    }
  }
}

Watcher

const Watcher = function(vm, fn) {
  const self = this;
  this.vm = vm;
  // 将当前Dep.target指向自己
  Dep.target = this;
  // 向Dep方法添加当前Wathcer
  this.addDep = function(dep) {
    dep.addSub(self);
  }
  // 更新方法,用于触发vm._render
  this.update = function() {
    console.log('in watcher update');
    fn();
  }
  // 这里会首次调用vm._render,从而触发text的get
  // 从而将当前的Wathcer与Dep关联起来
  this.value = fn();
  // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
  // 造成代码死循环
  Dep.target = null;
}

Watcher 实现了渲染方法 render 和 Dep 的关联, 初始化 Watcher 的时候,打上 Dep.target 标识,然后调用 get 方法进行页面渲染。

我们再拉通串一下整个流程:Vue 通过 defineProperty 完成了 Data 中所有数据的代理,当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图。

Dep的作用是什么?

用两个例子来看看依赖收集器的作用吧。

  • 例子1,毫无意义的渲染是不是没必要?

    const vm = new Vue({
        data() {
            return {
                text: 'hello world',
                text2: 'hey',
            }
        }
    })
    

    vm.text2的值发生变化时,会再次调用render,而template中却没有使用text2,所以这里处理render是不是毫无意义?

    针对这个例子还记得我们上面模拟实现的没,在Vuerender函数中,我们调用了本次渲染相关的值,所以,与渲染无关的值,并不会触发get,也就不会在依赖收集器中添加到监听(addSub方法不会触发),即使调用set赋值,notify中的subs也是空的。OK,继续回归demo,来一小波测试去印证下我说的吧。

    const vue = new Vue({
      data() {
        return {
          text: 'hello world',
          text2: 'hey'
        };
      }
    })
    
    vue.mount(); // in get
    vue._data.text = '456'; // in watcher update /n in get
    vue._data.text2 = '123'; // nothing
    
  • 例子2,多个Vue实例引用同一个data时,通知谁?是不是应该俩都通知?

    let commonData = {
      text: 'hello world'
    };
    
    const vm1 = new Vue({
      data() {
        return commonData;
      }
    })
    
    const vm2 = new Vue({
      data() {
        return commonData;
      }
    })
    
    vm1.mount(); // in get
    vm2.mount(); // in get
    commonData.text = 'hey' // 输出了两次 in watcher update /n in get
    

总结

  1. 从 new Vue 开始,首先通过 get、set 监听 Data 中的数据变化,同时创建 Dep 用来搜集使用该 Data 的 Watcher。
  2. 编译模板,创建 Watcher,并将 Dep.target 标识为当前 Watcher。
  3. 编译模板时,如果使用到了 Data 中的数据,就会触发 Data 的 get 方法,然后调用 Dep.addSub 将 Watcher 搜集起来。
  4. 数据更新时,会触发 Data 的 set 方法,然后调用 Dep.notify 通知所有使用到该 Data 的 Watcher 去更新 DOM。

参考文章

当面试官问你Vue响应式原理,你可以这么回答他

图解 Vue 响应式原理

Vue2 与 Vue3 响应式区别

  • Vue2的响应式是基于Object.defineProperty实现的
    • Vue2的第一个缺陷,无法监听数组变化。
      • 由于JavaScript 的限制,Vue2 不能检测以下数组的变动:
        • 当你利用索引直接设置一个数组项时,例如:vm.items[index] = newValue
        • 当你修改数组的长度时,例如: vm.items.length = newLength
    • Vue2的第二个缺陷是,Object.defineProperty,实现对象的深度监听,需要一次性递归到底。对于层级比较深的数据来说,计算量比较大。无法监听新增属性/删除属性,这也是为什么 Vue2 中对象新增属性需要使用 Vue.set(), 删除属性需要使用 Vue.delete()
  • Vue3的响应式是基于ES6的Proxy来实现的
    • Proxy可以劫持整个对象,并返回一个新的对象。
    • Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

注:

  • Object.defineProperty 本身是可以监听到利用索引直接设置值时的变化,但是 尤大大 考虑到性能代价和获得的用户体验收益不成正比,所以做了限制,通过重写pop、push、splice、shift、unshift、reverse方法,来实现数组的响应式。

    Vue2响应式原理解析和实现_第2张图片

  • Object.defineProperty 无法监听修改数组长度的变化。

你可能感兴趣的:(vue,Vue2,响应式原理,响应式原理,defineProperty)