从源码的角度分析vue computed的依赖搜集

JavaScript019

从源码的角度分析vue computed的依赖搜集,第1张

vue 源码版本是2.6.12

很多介绍vue源码的文章对computed怎么计算值讲的很清楚,但是对computed 怎么搜集到依赖它的视图渲染watcher,以及怎么去通知对应的渲染watcher去更新讲解的很模糊或者干脆一笔带过。这篇文章主要讲解——computed watcher是怎么搜集到订阅它的渲染watcher。

文件在src/core/instance/state.js

当组件读取computed a的值的时候会执行 computedGetter函数,先是通过

计算出computed函数的值,然后通过

进行依赖搜集。

Dep.target指向当前组件的渲染watcher,进入watcher.depend()看看是怎么进行依赖搜集的

文件位于 src/core/observer/watcher.js

第一个问题:this.deps的赋值

是在cleanupDeps函数中执行this.deps = this.newDeps,所以要看cleanupDeps在哪里被调用的,以及this.newDeps中的值是哪里产生的

get函数是在computed 通过watcher.evaluate()计算值的时候被调用的,讲解下这个函数的核心操作

这个this是计算属性的watcher,调用dep.js中的

作用是放到栈顶,同时将计算属性的watcher赋值给Dep.taget

会调用 计算属性a的函数

由于引用到了i,所以会触发i的get 函数,就会调用dep.depend(),实际上是i的依赖搜集,这里的dep对象属于i

dep.depend() 位于src/core/observer/dep.js

这里的Dep.target就是上面保存的computed watcher实例,会执行watcher中的addDep,这里的this就是i的dep实例

文件位于 src/core/observer/watcher.js

做了两件事

把栈顶的watcher弹出,改变Dep.target的指向,此时指向组件的渲染watcher

这一步就是 将this.newDeps的值赋给this.deps,此时this.deps中的数组中的对象其实就是i的dep实例

再回到 watcher.depend()

this.deps[i].depend() 这里就是执行

此时Dep.target是组件的渲染watcher,所以实现的逻辑是组件渲染watcher调用addDep(this),其实就是持有i的dep,最终被i搜集到依赖。

转了这么大一圈,实际上是为了让组件的watcher被计算属性中引用的data变量搜集到,这也不难理解,既然组件依赖computed的变化,当然也依赖computed中的值的变化,示例中computed中的值变化来自于i的变化,所以当i变化时,就让去通知计算属性的watcher去重新计算,通知组件watcher重新渲染。

对于data中变量的响应式原理和依赖搜集、派发更新可以参考我的这篇文章

从源码的角度分析Vue视图更新和nexttick机制

参考:

https://ustbhuangyi.github.io/vue-analysis/v2/reactive/getters.html#dep

https://juejin.cn/post/6877451301618352141

侦测状态变化,重新渲染页面。

拉(通知状态改变,然后暴力比对哪些节点需要重新渲染): Angular脏检查、React虚拟dom

推(明确知道哪些状态改变,细粒度,通知绑定这个状态的依赖节点更新): Vue

但,粒度越细,每个状态绑定的依赖越多,追踪开销就越大。从Vue2.0开始引入虚拟dom,绑定依赖到组件层面,而不是节点层面。 状态改变,通知到组件,组件内部再使用虚拟dom进行比对。

追踪变化 Object.defineProperty 和 Proxy

收集依赖

当数据发生变化的时候,需要通知使用了该数据的地方。所以在gettter中收集依赖,在setter中触发依赖。

为了减少耦合,封装Dep类,专门管理依赖

收集的依赖window.target,到底是啥?依赖是用到数据的地方,可能是模板,可能是用户写的一个watch,需要抽象出一个类集中处理多种情况,收集依赖阶段只收集这个类的实例,通知也只通知它,它再负责通知其他地方 -- Watcher。

递归侦测所有key

封装一个Observer类用于将data中的所有属性(包括子属性)都转化成getter/setter的形式。

getter/setter只能追踪一个属性是否被修改,但无法追踪新增和删除属性,所以另外提供了vm. delete两个api。ES6之前。

侦测Object变化是通过getter/setter实现的,但是如果用Array原型上的方法改变数组,就无法侦测了。同setter追踪,如果可以在用户使用Array原型上的方法改变数组时,得到通知,就可以侦测变化。

我们可以用一个拦截器arrayMethods去覆盖Array.prototype,在拦截器中发送变化通知, 再执行原本的功能。改变数组自身内容的7个方法: ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

拦截器arrayMethods不能直接覆盖Array.prototype,会污染全局的Array。我们的拦截操作只需要针对那些被侦测了变化的数据生效,也就是说拦截器只覆盖那些响应式数组的原型。将一个数据转化成响应式,需要用到Observer。

ES6用Object.getPropertyOf和Object.setPropertyOf替代了 proto 。

每次访问数组的值,就会触发getter。所以Array在getter里收集依赖,在拦截器中触发依赖。

依赖列表dep存储在Observer中,因为getter和拦截器中都可以访问到Observer实例。

getter中访问:

拦截器中访问:

这样,就可以通过数组值的 ob 属性访问到Observer实例上的dep,调用改变数组内容的方法时,通知依赖。同时,收集依赖中的observe函数中通过 ob 来判断,数据是否已经被Observer转换成了响应式。

侦测数组中元素变化

侦测新增元素变化

可以新增数组元素的方法为:push、unshift 和splice,可以取出新增元素,使用observeArray方法使其变成响应式的。

Array的变化侦测是通过拦截原型上方法实现的,所以对直接给数组某一项赋值,或者通过设置length改变数组,是侦测不到的。所以可以用api或方法代替。

expOrFn: a.b.c or 函数

options: { deep, immediate }

用于观察一个表达式或computed函数在Vue实例上的变化。回调函数调用时,会从参数得到newValue和oldValue。返回一个取消观察函数,用来停止触发回调。

deep: watch对象内部值的变化,都会触发回调

immediate: 立即以表达式的当前值触发回调

所有vm.$开头的属性,都是写在Vue.prototype上的。

原理

teardown 首先需要先在Watcher中记录自己被收录进了哪些Dep中,当unwatch时,遍历自己的记录列表,从dep依赖列表中把自己删除。

deep实现原理:除了要触发当前这个被监听数据的收集依赖之外,需要把其所有子值都触发一遍收集依赖。当子数据发生变化时,可以通知当前Watcher。

在taget上设置一个属性,如果target是响应式的,被创建的属性也是响应式的,并触发视图更新。主要用来避免vue侦测不到新增加属性的限制。

用于删除target对象上的key属性。如果对象是响应式的,需要确保删除能触发更新试图。主要为了避免直接使用delete无法被侦测到变化的限制。