vue2-数据响应式原理

1,065 阅读3分钟

响应式

响应式是指状态的(对象的属性)修改之后,能自动更新到视图上。这样我们就可以集中精力在具体的业务操作上,并不需要关心dom。

vue主要是通过:数据劫持 + 依赖收集(这里用到了发布者-订阅者模式)的方式来实现的。

第一是:数据劫持,又叫数据拦截。vue2是通过Object.defineProperty来将对象的每一个属性转化成setter,getter。其中修改对象的属性时,就会触发setter,这样可以知道哪个属性被修改了。

第二是:依赖收集。就在渲染视图时,将 Watcher和具体的属性,通过发布者订阅者模式管理起来,这样数据改变之后,就能精准更新视图。

收集依赖

vue的整个工作过程

  1. 挂载前。对data对象进行遍历生成对应的setter,getter。在getter中维护了一个用来收集依赖的对象。
  2. 挂载时
    • 把template/el 转成 render函数
    • 创建updateComponent函数,在这个函数中会调用render函数
    • 给每个组件/vue实例 new Watcher,并传入updateComponent函数
      • 在构造器内,调用一次updateComponent
      • 当watcher调用时,就会调用updateComponent函数
  3. 挂载后
    • 用户更新数据就触发对应的watcher,再次更新视图

收集依赖的过程就发生的new watcher的构造器内,它会调用render函数,而render会访问data中的属性,进而触发getter,在getter内部就完成了依赖收集

每new一个watcher时,就会给Watcher添加一个ID,这样在添加依赖的会先判断这个ID是否有重复的,以免重复收集。

特殊处理数组的响应式

Vue2.0内部并没有直接逐一去给数组元素添加响应式功能:如果你直接通过下标去向数组中传入新值,vue并不能监测到。其实Object.defineProperty是可以监控对数组的修改的,如下代码:

var arr = [1,2]

for(var k in arr){
    console.log(k)
    Object.defineProperty(arr, k, {
        getter() {
            return arr[k]
        },
        setter(newVal) {
            if(newVal !== arr[k]){
                return
            }

            console.log('setter ....', newVal) 
        }
    })
}

arr[0] = 100 // 能触发setter

vue2.0之所以没有这么做是因为数组元素的个数是不可控的(想象一下1000个元素的数组),并且可以随时增减的。

vue2.0中,对于数组的处理是拦截了数组上的7个方法push,pop,shift,unshift,splice,sort,reverse,也就是说只有调用如上7个方法中的某一个才能触发响应式。

为啥是这7个呢? 因为只有它们才会修改源数组

核心代码如下:

  var arrayProto = Array.prototype;
  var arrayMethods = Object.create(arrayProto);

  var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];

  /**
   * Intercept mutating methods and emit events
   */
  methodsToPatch.forEach(function (method) {
    // cache original method
    var original = arrayProto[method];
    def(arrayMethods, method, function mutator () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      if (inserted) { ob.observeArray(inserted); }
      // notify change
      ob.dep.notify();
      return result
    });
  });
  
  function protoAugment (target, src) {
    /* eslint-disable no-proto */
    target.__proto__ = src;
    /* eslint-enable no-proto */
  }
  
  var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };
  

以拦截push方法为例,其简化理解代码如下:

const arrayMethods = Object.create(Array.prototype)
Object.defineProperty(arrayMethods,'push', {
    value:function(...arg){ 
        const res = Array.prototype.push.apply(this,arg) // 执行原来的push
        // 1. 对新数据做响应式处理
        console.log('对新数据做响应式处理')
        // 2. 通知观察者们去执行
        console.log('通知观察者们去执行')
        
        return res
    }
})

var arr = [1,2,3]
arr.__proto__ = arrayMethods
arr.push(4)

数组的操作替代

问题1: 通过下标直接修改不能响应式

2种方式都可以实现下标修改数据还能响应式: (1) Vue.set,或者this.$set

// Vue.set
Vue.set(this.arr, idx, 新值)

(2) 借用splice方法:删除一元素,接着添加一个元素

// Array.prototype.splice
this.arr.splice(indexOfItem, 1, newValue)

问题2: 直接修改length属性没有响应式

可以使用 splice

this.arr.splice(你要设置的数组长度)