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方法去求值并缓存起来。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值