Vue源码之计算属性watcher

Posted 天地会珠海分舵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue源码之计算属性watcher相关的知识,希望对你有一定的参考价值。

在之前的文章《Vue源码分析基础之响应式原理》和《Vue源码实现之watcher拾遗》中,我们学习了watcher的实现原理。紧跟着这几天准备花点时间学习下watcher在vue框架中的应用。

纵观整个vue源码,使用到watcher的地方应该有三个

  • 计算属性watcher: vue会为我们在computed选项写的每个我们自定义的计算属性创建一个计算属性watcher,该watcher会在依赖的响应式数据变化时将计算属性标志位设置成dirty,使得页面在下次更新时调用计算属性函数进行求值。
  • 渲染watcher: vue会为每个组件创建一个渲染watcher来在依赖的响应式数据状态发生变化时重新渲染页面
  • 用户watcher: vue会为我们在watch选项写的每个要监控的属性创建一个watcher来在其变化时执行提供的回调函数

今天我们先来看下计算属性watcher。

首先我们先看下我们通常写computed计算属性是怎么写的

1. 计算属性的两种写法

计算属性提供两种写法,一种是函数写法:

computed: 
  twiceCounter: function () 
    return this.counter * 2;
  

另外一种是选项写法,可以提供计算属性的setter和getter

computed: 
  twiceCounter: 
    get() 
      return this.counter * 2;
    ,
    set(value) 
      this.counter = value / 2;
    ,
  ,
,

其中setter使用甚少,我这么几年vue使用下来几乎没有用过,所以基本可以忽略不管。这里为了统一,我们将第一种写法中的函数和第二种写法中的get方法统称为计算属性求值函数

2. 计算属性和计算属性watcher的实现原理

2.1. 计算属性初始化

在vue实例或者组件实例对象初始化时,将会调用一系列的初始化函数来初始化我们编写的data,watch,computed这些选项,其中初始化computed的方法叫做initComputed:

const computedWatcherOptions =  lazy: true ;
function initComputed(vm: Component, computed: Object) 
  const watchers = (vm._computedWatchers = Object.create(null));
 
  for (const key in computed) 
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;
    ...
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    );

    if (!(key in vm)) 
      defineComputed(vm, key, userDef);
     
    ...

参数vm就是我们的组件实例对象,computed就是我们前面例子中我们自定义的computed选项,选项下面就是我们自定义各个计算属性twiceCounter之类的。

注意这里会在组件实例对象vm下创建一个_computedWatchers的数组,我们紧跟着要创建的每个计算属性watcher都会以该计算属性的名字为key保存到其中。

这里for循环就是遍历我们编写的computed下面的每个计算属性,将计算属性的内容赋值给userDef,然后开始用计算属性求值函数来初始化getter:当该计算属性的写法是函数式写法的时候,getter直接被赋予为该函数;如果计算属性是提供了getter和setter的选项写法的话,getter被赋予该计算属性自身的get方法。

2.2. 为每个计算属性创建一个计算属性watcher

跟着就是调用Watcher构造函数去创建watcher。注意第二项参数为getter及最后一个选项参数为 lazy: true 。

export default class Watcher 
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) 
    this.vm = vm;
    ...
    // options
    if (options) 
      ...
      this.lazy = !!options.lazy;
    
    ...
    this.dirty = this.lazy; // for lazy watchers
    ...
    
    // parse expression for getter
    if (typeof expOrFn === "function") 
      this.getter = expOrFn;
     else 
      ...
    
    this.value = this.lazy ? undefined : this.get();
  

首先,要注意的是这里的计算属性watcher的dirty属性为true,因为它是自己从options.lazy中取值的,而lazy我们在前面看到是被设置成true的。注意这里dirty为true,我们后面的分析会用到。

跟着,计算属性的求值函数将会被赋予给计算属性watcher的getter,该方法在watcher的get方法中调用。紧跟着我们看到get方法不会在计算属性watcher的构造函数中调用,因为lazy属性为true。

那么get方法在什么时候调用呢?毕竟,我们知道get方法在watcher中是承担了依赖收集这个关键的任务的。

别急,我们很快就会看到了。

这里我们先结束掉计算属性watcher的创建,接着往下分析intComuted方法。

2.3. 在组件实例对象上定义同名计算属性并将其getter设置成computedGetter

从代码可见其紧跟着对每个计算属性都调用了一个叫做defineComputed的方法

export function defineComputed(
 target: any,
 key: string,
 userDef: Object | Function
) 
 const shouldCache = !isServerRendering();
 if (typeof userDef === "function") 
   sharedPropertyDefinition.get = shouldCache
     ? createComputedGetter(key)
     : createGetterInvoker(userDef);
   sharedPropertyDefinition.set = noop;
  else 
   sharedPropertyDefinition.get = userDef.get
     ? shouldCache && userDef.cache !== false
       ? createComputedGetter(key)
       : createGetterInvoker(userDef.get)
     : noop;
   sharedPropertyDefinition.set = userDef.set || noop;
 
 ...
 Object.defineProperty(target, key, sharedPropertyDefinition);


这个函数主要的作用就是通过defineProperty方法来在target,即我们的组件实例对象vm上,建立与对应计算属性同名的一个属性,而该同名属性的访问是sharedPropertyDefinition的getter和setter来劫持处理的。比如我们前面例子中的twiceCounter,在通过this.twiceCounter访问时,访问到的将是sharedPropertyDefinition中的getter和setter。

注意我们这里分析的不是服务器渲染SSR的情况,所以shouldCache为true,这样一来,无论我们的计算属性使用哪种写法,都将会以该计算属性名称为参数来调用createComputedGetter方法,而该方法是个高阶函数,将返回一个方法来作为该计算属性的getter。

至于setter,如果我们使用的是选项式的方式写的计算属性,且提供了setter的话,则直接使用该setter,否则为noop,即一个空函数。

2.4. computedGetter实现原理

下面我们就来看下createComputedGetter是怎么为计算属性生成computedGetter

function createComputedGetter(key) 
  return function computedGetter() 
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) 
      if (watcher.dirty) 
        watcher.evaluate();
      
      if (Dep.target) 
        watcher.depend();
      
      return watcher.value;
    
  ;

很明显该方法是个高阶函数,因为它返回的就是一个叫做computedGetter的函数。且该函数的名字也很醒目,computedGetter,计算getter,计算属性getter,挺吻合的。

computedGetter首先从我们前面initComputed时保存到_computedWatchers的对应计算属性watcher给拿出来。

然后判断dirty是否为true。往回翻下watcher的构造函数那段,这时dirty是被设置成和dirty选项一样的值,也就是true。

所以这里就会调用计算属性watcher的evaluate方法,其实这就是一个给计算属性求值的过程:

evaluate() 
   this.value = this.get();
   this.dirty = false;
 

该方法我能查到的唯一的调用也就只在这里,估计也就是专门给计算属性用的。

很明显该方法直接调用了watcher的get方法,这个方法我在本系列之前的文章就已经分析过,就是用来调用watcher的getter来进行求值并进行依赖收集用的,这里就不赘述了。

get() 
    pushTarget(this);
    let value;
    const vm = this.vm;
    try 
      value = this.getter.call(vm, vm);
     catch (e) 
      if (this.user) 
        handleError(e, vm, `getter for watcher "$this.expression"`);
       else 
        throw e;
      
     finally 
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) 
        traverse(value);
      
      popTarget();
      this.cleanupDeps();
    
    return value;
  

这里有一点还是要提下的是,作为计算属性watcher,这里的getter是我们写的计算属性函数,无论是哪种写法,我们都需要写return来返回一个值作为这个计算属性最终的值的,不记得返回文章前面再看下twiceCounter示例的写法。

所以这里的getter调用就是我们计算属性getter中的返回值,这个计算结果最终会返回给上面的evaluate方法,并保存到watcher的value属性中。

下面我们继续看computedGetter的剩余部分。

紧跟着就是通过计算属性watcher的depend方法将渲染watcher加入到计算属性watcher所依赖的所有属性的dep.subs中。这里有点拗口,如果不理解的可以看下《Vue源码实现之watcher拾遗》中说Watcher设置依赖收集标志时为什么要pushTarget和popTarget的章节,里面有对这个方面有更详尽的描述。

紧跟着computedGetter就会将前面经历getter后保存到计算属性watcher的value中的值给返回,最后我们在组件实例对象看到的该计算属性名称的值就是这个computedGetter的返回值,兜了一大圈,其实也就是我们最前面的例子中写的计算属性的getter的返回值。

2.5. 为什么说计算属性是惰性求值

我们经常会听到说计算属性是惰性求值的这样的说法,意思是说我们如果页面模板上多次引用了计算属性,那么只有第一次引用的时候会执行我们写的计算属性对应的getter方法,其他引用都不会再进行计算,而是直接返回第一次引用的结果。只有在计算属性改变的时候,才会再次计算。

经过上面的分析,我们访问vm上的计算属性,事实上会调用computedGetter,函数里面对计算属性watcher的dirty属性就是这里理解惰性求值的关键。

如果这个dirty一直是true,那么肯定不会去执行evaluate,也就是不会真正的去求值,而是在后面直接返回watcher上次保存下来的value。

只有当dirty为false的时候,才会真正执行watcher的evaluate方法来调用我们自己写的计算属性函数来求值。

从上面的分析,我们知道计算属性初始化的过程中会在watcher构造函数把dirty设置成true,这样在计算属性初始化过程就会在evaluate函数中进行一次求值,求值完后立刻将watcher设置成false。

但是我们没有分析到该dirty属性在什么时候再次被设置成true。

其实时间点是在所依赖的属性被修改的时候。一旦依赖属性被修改,就会触发订阅的对应的计算属性的watcher,调用其update方法。

 update() 
    /* istanbul ignore else */
    if (this.lazy) 
      this.dirty = true;
     else if (this.sync) 
      this.run();
     else 
      queueWatcher(this);
    
  

因为我们的lazy在初始化时就提供为true,所以这里就会再次将dirty设置成true。

也就是说,一旦依赖属性被修改了,对应计算属性的watcher就会被设置成true,从而导致在下次访问计算属性时,computedGetter会调用计算属性watcher的evaluate方法去求值并缓存起来。

完整的流程就是,所依赖的数据发生变化,比如例子中的counter发生变化时,就会notify对应的计算属性watcher,并在update方法中将dirty设置成true。跟着vue就会触发渲染watcher,然后调用updateComponent,跟着调用组件的render方法重新生成虚拟DOM, 此时就会去读模板引用到的计算属性如这里的twiceCounter,这时就会触发computedGetter的调用,computedGetter发现计算属性watcher的dirty属性为true,就会调用计算属性watcher的evaluate方法去求值并缓存起来。

我是@天地会珠海分舵,「青葱日历」和「三日清单」作者。能力一般,水平有限,觉得我说的还有那么点道理的不妨点个赞关注下!

以上是关于Vue源码之计算属性watcher的主要内容,如果未能解决你的问题,请参考以下文章

Vue源码之渲染watcher

Vue源码之渲染watcher

手写Vue源码之计算属性

Vue源码实现之watcher拾遗

Vue源码实现之watcher拾遗

Vue源码之用户watcher