vue响应式原理源码实现之Watcher和Dep,computed属性和watch属性的实现原理

当我们把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项时,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty把这些 property 全部转为 getter/setter。在这之后,每次我们对data对象里的property赋值时,相应的setter方法会被调用,而setter方法里包含了通知vue更新视图的代码,因此接下来vue就会根据最新的data生成新的虚拟dom树,和旧的树做比较并更新有变化的地方。

那么,是只要对data里任何的property赋值都会触发vue组件的更新流程吗?

其实不是的,只有对被 ''用'' 到了的property进行赋值才会触发变更检测。假如data中有属性prop1和prop2,而组件的template中只绑定了prop1,这时候我们对prop2赋值就不会触发变更检测,因为template和prop2没有依赖关系。

以上这些都是vue官网对响应式原理的描述。而作为一个好奇的程序猿,自然会好奇vue中对这种依赖收集和变更检测的触发具体是如何实现的,我就去研究了一下vue的源代码。

Watcher 和 Dep

通过阅读源代码发现每个组件实例都会绑定一个watcher对象,保存在组件实例的_watcher属性中。

var Watcher = function Watcher (
  vm, 
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  //vm是创建watcher对象时传递进来的组件实例
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }

  ......省略代码

};

而组件中data和props里的所有属性都会绑定一个dep对象。

function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  // 创建Dep对象实例,下面属性的setter方法和getter方法能通过闭包作用域访问到该对象
  var dep = new Dep();

  ......省略代码

  var property = Object.getOwnPropertyDescriptor(obj, key);
  
  ......省略代码

  var childOb = !shallow && observe(val);
  // 为属性设置getter,setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {

     ......省略代码
      
      if (Dep.target) {
        // 在getter方法中调用Dep.depend()让该属性成为Dep.target的依赖,Dep.target的值为watcher对象
        dep.depend();

        ......省略代码

      }

      ......省略代码

      return value
    },
    set: function reactiveSetter (newVal) {

      ......省略代码
      // 在setter方法中调用dep.notify()通知被依赖的wathcer对象调用wathcer.get()方法
      dep.notify();
    }
  });
}

当组件初始化或更新视图时会把Dep.target的值(在此可以吧Dep.target理解成一个全局变量),设置成组件绑定的wathcer对象。而组件初始化要生成虚拟dom树就必定要读取绑定到template中的property的值,那么getter方法就会被调用,在getter方法里会调用dep.depend()方法。

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

最终,Dep.target保存的wathcer对象会被push到dep.subs数组中。而在属性的setter方法里,dep.notify()方法被调用。该方法会遍历dep.subs数组中的wathcer对象并调用watcher.update()方法。

Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};
......
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this); // 把watcher放到异步队列中
  }
};

最终,与组件绑定的watcher对象的getter回调方法会被调用,该getter回调方法是在实例化对象时作为构造参数传入的。组件级wathcergetter方法是一个叫updateComponent的方法,这会导致组件检测变化并更新视图。

Watcher.prototype.get = function get () {
  pushTarget(this); // 把Dep.target的值设置为本wathcer

  ......省略代码

  try {
    value = this.getter.call(vm, vm);// 调用getter回调方法
  } catch (e) {
     ......省略代码
  } finally {
     ......省略代码
  }
  return value
};
......省略代码
updateComponent = function () {
    vm._update(vm._render(), hydrating);
};

最后我们不难想到那些没被用到的data属性因为在组件初始化时其getter方法没被调用,所以其绑定的dep.subs数组中没有组件的wathcer对象, 所以即使对其赋值会导致dep.notify()的调用也不会触发vue组件的变化更新。(vue组件在初始化视图或在视图更新时会调用pushTarget(this)把Dep.target设置为组件的wathcer,更新完成后会调用popTarget()把Dep.target设置为前一个值或null,所以在此之后再调用dep.depend()并没有什么效果)

computed属性和watch属性的实现原理

computed属性和watch属性实现原理的核心也是WatcherDep,先来具体看一下computed属性。

computed属性

组件在初始化时会遍历computed属性,并为每一个computed属性绑定一个watcher对象,而创建wathcer对象时传入的getter回调即为computed属性的方法。

function initComputed (vm, computed) {
  var watchers = vm._computedWatchers = Object.create(null);
  
  ......省略代码

  for (var key in computed) {
    var userDef = computed[key];
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    
    ......省略代码

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    ......省略代码
}

当我们要读取computed属性的值时,watcher.get()方法调用。该方法会先调用pushTarget(this)Dep.target的值设置为当前wathcer, 然后调用getter回调(即computed属性的方法)。如果getter回调方法体内有读取data属性或props属性的值,则它们会在自己的getter方法里调用dep.depend(),然后这些属性和computed属性就会形成依赖关系。而wathcergetter回调函数返回的值会被保存到wathcer.value中,并且会把wathder.darty置为false。

computedwatcher和组件级wathcer不同的是computedwatcher.lazy属性为true,这意味着wathcer.get()调用后不会马上调用wathcergetter回调,而是会先检测wathcer.darty是否为true,若不为true则立马返回wathcer.value(上一次调用getter回调返回的值),若为true则重新调用getter回调计算新的值并保存在wathcer.value中。

当对和computed属性有依赖关系的data和prop属性进行赋值操作后,dep.notify()调用,这会导致wathcer.dirty被置为true。这样当下次要获取computed属性的值时,computed方法会重新计算出新的值并保存到wathcer.value中。

所以不是每次获取computed属性的值时computed属性的方法都会执行,而是当computed属性的方法依赖的属性被重新赋值后才会重新执行。

watch属性

同样的在初始化组件时会遍历wwath属性被为每一个watch属性创建一个watcher对象,并在创建watcher对象时传入watch属性的名称和watch回调方法, 然后把该wathcer对象push到相应的data属性和prop属性的dep.subs中。当dep.notify()调用后,watch回调会被调用。

你可能感兴趣的:(vue响应式原理源码实现之Watcher和Dep,computed属性和watch属性的实现原理)