Vue源码之computed

581 阅读8分钟

computed

抛出问题

  1. computed何时创建,创建做了啥

  2. computed的key渲染访问时如何触发它的get

  3. computed Watcher创建流程会做啥

  4. 页面P 与 computed A 与 data B 三者不清不楚的依赖关系是啥

  5. data B发生改变后如何重新渲染页面

  6. lazy与dirty什么乱七八糟的关系

computed何时创建,创建做了啥

  1. initState时,会去处理各种选项,其中包括处理 computed

if (opts.computed) initComputed(vm, opts.computed)

  1. initComputed

函数⾸先创建 vm._computedWatchers 为⼀个空对象,接着对 computed 对象做遍历,拿到计算属性的每⼀个key对应的值,如果是函数就赋值给getter否则获取get属性赋值给getter。

如果getter == null 报错。

接下来为每⼀个 getter 创建⼀个computer watcher 传入注意computedWatcherOptions = { lazy:true }。

最后对判断如果 key 不是vm 的属性,则调⽤ defineComputed(vm, key, userDef) ,否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占⽤,如果是的话则在开发环境报警告。

  1. defineComputed

利⽤ Object.defineProperty 给计算属性对应的 key 值添加 getter 和setter,setter 通常是计算属性是⼀个对象,并且拥有 set ⽅法的时候才有,否则是⼀个空函数。在平时的开发场景中,计算属性有 setter 的情况⽐较少,最终 getter 对应的是 createComputedGetter(key) 的返回值。

  1. createComputedGetter

createComputedGetter 返回⼀个函数 computedGetter ,它就是计算属性对应的 getter。整个计算属性的初始化过程到此结束。

computed的key渲染访问时如何触发它的get

在上面可以看到 此时调用Object.defineProperty订阅了key,而此时的sharedPropertyDefinition是一个对象,对象内有个get为computedGetter()函数。由于订阅了到时渲染到key时就会调用get。

computed Watcher创建流程会做啥

可以发现 computed watcher 并不会⽴刻求值因为此时lazy为true,注意此时lazy赋值给了dirty。

页面P 与 computed A 与 data B 三者不清不楚的依赖关系是啥

  1. 猜想

理论上B依赖于A A依赖于P ->所以A收集了P的Watcher B收集了A的Watcher。
理论上data B改变时,会通知computed Watcher进行更新,然后computed Watcher通知页面Watcher进行更新。

  1. 然而往往现实是很残酷的

B依赖于A A依赖于P -> 实际B收集了A的Watcher和P的Watcher。
data B改变时,会通知computed Watcher进行更新,然后data B通知页面Watcher进行更新。(不禁发出妙哉妙哉)

????黑人问号 data B是怎么收集到P的Watcher的。

当开始页面初始化时,创建页面的Watcher,页面Watcher的getter是updateComponent,创建页面Watcher时执行this.get(),targetStack推入页面Watcher,Dep.target赋值为页面Watcher,执行getter,也就是执行updateComponent,也就是中vm._render,当渲染到computed的key时,由于key已做响应式所以触发key的get,也就是上面所说的computedGetter()函数。

  1. computedGetter

这里分两步走

  • 第一步

根据Key获取到component Watcher,判断watcher.dirty是否为true,是执行evaluate,valuate判断 this.dirty ,首先通过this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,又会执⾏this.getter.call(vm, vm) ,这实际上就是执⾏了计算属性key的值也就是执行我们写的回调,由于data B是响应式,当我们访问时,我们就会触发它的get。触发dep.depend(),targetStack推入component Watcher,Dep.target赋值为component Watcher,最终就是data B收集到了component Watcher,注意注意骚操作来了,此时会继续执行popTarget(),推出targetStack的computed Watcher,此时Dep.Watcher为页面PWatcher,然后执行第二步!

  • 第二步
    如果存在Dep.Watcher执行watcher.depend,调用dep.depend,结果往此key的dep.subs中添加了页面 Watcher所以此key此时存在computed和页面Watcher。这一进一出,像不像极了那个哈塞给!

然后呢 然后就是值也拿到了依赖也收集完了然后就vm._update创建节点到页面中咯。

页面P 与 computed A 与 data B 三者不清不楚的依赖关系是啥

由上可得,data B收集了computed A的Watcher和页面p的Watcher,只是有两个追求者而已不是那种不三不四的关系,坐下。

data B发生改变后如何重新渲染页面

data B发生变化就会触发set,修改数据触发set值未改变直接返回。

值改变了赋值然后触发dep.notify()去重新渲染,dep.notify()就是调用watcher的update。

然后触发computed Watcher和页面Watche的update。
computed Watcher的update就单单的是把 this.dirty = true 完事!
页面Watche的update就会执行queueWatcher(),最终就是执行页面watcher的run,watcher.run又会去调用this.get(),然后pushTarget(this),然后执行getter也就是updateComponent!后面的你就知道了就是重复渲染获取值得过程。

lazy与dirty什么乱七八糟的关系

由上可得, lazy 表示一种固定描述,不可改变,表示这个 watcher 需要缓存,而 dirty 表示缓存是否可用,如果为 true,表示缓存脏了,需要重新计算,否则不用。dirty 默认是 false 的,而 lazy 赋值给 dirty,就是给一个初始值,表示你控制缓存的任务开始了所以记住,dirty才是真正的控制缓存的关键,而lazy只是起到一个开启的作用。

总结

来我们上一个麻花硬菜 看哭了这张图的点个赞吧,哈哈哈哈哈!

1.在初始化数据InitState如果定义了computed就会有个initComputed的操作,initComputed首先创建watchers和 vm._computedWatchers为空对象,接着遍历用户定义的computed获取到key对应的value,如果value为函数直接赋值给getter,否则去获取value的get属性赋值给getter。接着判断getter是否存在不存在直接报错。接着为创建的watchers对象添加key属性对应的值为new Watcher实例。

2.new Watcher实例传入getter也就是回调以及computedWatcherOptions配置项设置lazy为true。

3.Watcher首先将传入的lazy赋值给this.dirty。然后将传入getter也就是回调赋值给 this.getter。接着因为this.lazy为true所以不会立刻执行this.get()。

4.回到initComputed最后处理computed中的键值与data中的数据冲突如果存在冲突则报错,不存在冲突则调用defineComputed传入vm实例,每个computed定义的key,以及key对于的value。

5.defineComputed,首先有这么一个sharedPropertyDefinition对象设置了对象的enumerable,configurable,get,set属性。接着判断传入的value是否为函数,如果是函数设置sharedPropertyDefinition的get为createComputedGetter(key)。如果不是函数那么也就是对象的话,如果value存在get函数,设置sharedPropertyDefinition的get为createComputedGetter(key),如果value存在set函数,设置sharedPropertyDefinition的set为value.set函数。

6.createComputedGetter它的结果是返回computedGetter函数等待触发。

7.回到defineComputed最后调用了Object.defineProperty(target, key, sharedPropertyDefinition),在vm实例上对computed当前key做响应式处理。设置了key的get为createComputedGetter。

8.页面执行到mount过程会创建页面Watcher,然后会调用this.get()会把,把Dep.watcher设置为页面Watcher,将页面Watcher丢入targetStack队列中。执行getter也就是updateComponent也就是 vm._update(vm._render(), hydrating)},然后就是render解析到computed的key触发key的get方法。也就是触发了createComputedGetter。

9.createComputedGetter首先获取到实例的_computedWatchers对象(在上面第一步时的操作)通过key拿到缓存的watcher,然后第一步判断watcher.dirty一开始dirty为true,那么就会去调用 watcher.evaluate()方法。第二步判断是否存在Dep.target,如果存在那么就会去调用watcher.depend()方法。最后返回watcher.value。

10.对于evaluate,首先设置了value的值为this.get(),然后将dirty 设置为false。this.get()顾名思义首先执行pushTarget(this)把Dep.watcher设置为computed Watcher将computed Watcher丢入targetStack队列中。接着去执行了this.getter()。这里的getter不在是updateComponent而是computed key创建computed Watcher传入key对应的回调。执行回调访问到了新的数据 触发新数据的get,首先取值返回接着就是判断是否存在Dep.target,此时存在Dep.target为computed Watcher,那么就会去调用dep.depend()方法。最终就是新的数据的dep的subs数组中收集到了了computed Watcher。this.getter执行完毕后紧接着执行popTarget(骚操作),推出targetStack的最后一位也就是computed Watcher,然后设置Dep.Watcher为页面PWatcher。然后就是cleanupDeps清理优化。

11.evaluate执行完毕后执行第二步,此时存在Dep.target为页面的Watcher,会执行watcher.depend()方法,首先遍历deps去调用他们各自的depend方法。结果就是往此key的dep.subs中添加了页面 Watcher所以此key此时存在computed和页面Watcher。

12.最后就是回到updateComponent render触发get函数的getter执行完毕,执行popTarget,推出targetStack的最后一位也就是页面Watcher,然后设置Dep.Watcher为空。然后就是cleanupDeps清理优化。收集依赖完成。

13.当去修改数据时触发set方法首先会判断值是否发生改变,为改变直接返回,改变了触发dep.notify()通知更新,会去执行watcher.update,对于computed watcher存在lazy为tue,然后把dirty设为true结束。对于页面Watcher执行queueWatcher,最终执行watcher.run执行this.get()执行getter执行updateComponent,然后就是render解析到computed的key触发key的get方法。也就是触发了createComputedGetter。接着就是上面createComputedGetter的操作了。