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的主要内容,如果未能解决你的问题,请参考以下文章