vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)
简介
前面我们分析了vue2中对象和数组的响应式原理。今天我们再来说说vue2中的依赖收集。
依赖收集我们都知道,就是需要触发响应式数据的get方法,但是在初始化的时候,我们并没有去触发get方法,那每个数据的get方法到底是在什么时候被触发的呢?都有哪些方式触发get呢?
带着这些疑问,我们通过阅读源码的方式来一探究竟。😉
本文vue版本为2.6.14
defineReactive
前面我们说到,依赖收集主要是在defineReactive方法里面的get方法。这里我们重点来看下这个方法。
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 实例化 dep,一个 key 一个 dep
const dep = new Dep()
// ...
// 这里很重要,如果不是浅监听,则递归监听
// 如果值是对象,则又会进行监听
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 获取值
const value = getter ? getter.call(obj) : val
// 依赖收集
if (Dep.target) {
// 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
dep.depend()
// 有 childOb 表示当前value是对象
if (childOb) {
// 继续进行依赖收集,其实就是将同一个watcher观察者实例放进了两个dep中,一个是正在本身闭包中的dep,另一个是子元素的dep
childOb.dep.depend()
// 如果当前value还是数组
if (Array.isArray(value)) {
// 将数组每一项都进行依赖收集
dependArray(value)
}
}
}
return value
},
})
}
为了方便理解我把 dependArray贴出来。
dependArray
遍历数组,对每一项元素进行依赖收集,如果子元素还是数组,则会进行递归处理。
// src/core/observer/index.js
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
在这里我们重点关注三个点
第一 const dep = new Dep(),每个key都会有一个dep对象与之对应。
第二 如果有Dep.target 则执行 dep.depend()。
第三 如果有childOb即执行childOb.dep.depend()。并且如果是数组的话还会继续执行dependArray。
这里为什么要执行childOb.dep.depend(),这个笔者会在扩展部分进行说明。
那 Dep 到底是个什么东西呢?我们来看看
Dep
Dep主要是用来管理Watcher的。
// src/core/observer/dep.js
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
export default class Dep {
// 静态属性 target,值为Watcher对象
static target: ?Watcher;
id: number;
// watcher数组
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 在 dep 中添加 watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 在 dep 中删除 watcher
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 向 watcher 中添加 dep 又向 dep 中添加 watcher
depend () {
// 其实调用的是watcher里面的addDep方法
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 派发更新
// 通知 dep 中的所有 watcher,执行 watcher.update() 方法
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 排序
subs.sort((a, b) => a.id - b.id)
}
// 遍历 dep 中存储的 watcher,执行 watcher.update()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
// 用数组模拟栈数据结构
const targetStack = []
// 在需要进行依赖收集的时候调用,设置 Dep.target = watcher
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
// 依赖收集结束调用,设置 Dep.target为栈顶的那个watcher
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
在Dep中,主要有如下重点
- 有一个静态属性
target,类型为Watcher。这是一个非常巧妙的设计,因为在同一时间只能有一个Watcher被计算。Dep.target = 当前正在执行的 watcher。 - 有一个
subs数组,用来存放watcher。 - 有
depend方法,实际上调用的是watcher里面的addDep方法。在addDep中实际上是一个双向添加的过程。既向watcher中添加dep又向dep中添加watcher。 - 因为有父子组件的存在(洋葱模型),所以这里巧妙的设计了一个
targetStack栈,调用pushTarget方法的时候将watcher推入栈中并给Dep.target赋值。调用popTarget方法的时候将最上面的watcher弹出,并给Dep.target赋值栈顶watcher。
接下来我们再来看看与之关联的Watcher。
Watcher
我们知道,在初始化的时候vue会遍历data,重写属性的get、set方法进行数据劫持。但是并没有去触发get、set方法,因此,并没有进行依赖收集和派发更新。
真正的依赖收集其实是在Watcher中。因为Watcher会触发数据的get方法。
因为本文只探讨依赖收集,所以笔者只贴出了Watcher依赖收集相关代码。
// src/core/observer/watcher.js
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
invokeWithErrorHandling,
noop
} from '../util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '../util/index'
let uid = 0
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// computed的lazy会是true,所以第一次他不会计算,而是在需要使用的时候才会计算
this.value = this.lazy
? undefined
: this.get()
}
// 依赖收集
get () {
// 将 Dep.target = this
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 {
if (this.deep) {
traverse(value)
}
// 将 Dep.target 设置为栈顶的watcher
popTarget()
this.cleanupDeps()
}
return value
}
// 这是一个双向添加的过程
// 添加dep到newDeps,然后又将自身watcher添加到subs数组
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// watcher 加 dep
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// dep 加 watcher
dep.addSub(this)
}
}
}
// 清除依赖收集
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
// ...
}
在详细分析Watcher之前,我们先要搞清楚,在vue中到底有几种watcher。
watcher种类
在vue中,总共有三种watcher。
渲染watcher
第一种是渲染watcher,每一个组件都会对应一个渲染watcher。在beforeMounte之后mounted之前,会初始化一个watcher。

computed watcher
第二种是computed watcher,它会遍历 computed 中的每个 key,向 computed watchers 列表中新增一个 watcher 实例。

user watcher
第三种是 user watcher,它会遍历 watch 中的每一个 key,调用 vm.$watch 创建一个 watcher 实例。

因为渲染watcher才是依赖收集的大头,所以下面笔者以渲染watcher为例,来详细分析下依赖收集的过程。
渲染watcher依赖收集详细过程
每个组件在mount前都会执行 mountComponent 函数,其中有一段比较重要的逻辑,大致如下:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
每一个组件都会有一个渲染watcher与之对应。
当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数,首先会执行:
pushTarget(this)
实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(因为父子组件绑定是洋葱模型,这里巧妙的使用了栈数据结构)。接着又执行了:
value = this.getter.call(vm, vm)
this.getter 对应就是 updateComponent 函数,这实际上就是在执行:
vm._update(vm._render(), hydrating)
这就是真正的依赖收集,它会先执行 vm._render() 方法,render 方法会生成 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 get。
每个对象值的 get 都持有一个 dep,在触发 get 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。
我们再来看看addDep方法
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
addDep方法会做一些逻辑判断(保证同一数据不会被添加多次),将dep对象添加到当前watcher的newDeps数组中,后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。
这个方法是一个双向添加的过程,既把dep添加到watcher,又把watcher添加到dep。
所以在渲染函数执行生成VNode的过程中,会触发所有用到数据的 get,这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了么,其实并没有,在完成依赖收集后,还有几个逻辑要执行,首先是:
if (this.deep) {
traverse(value)
}
这个是要递归去访问 value,触发它所有子项的 getter,接下来执行:
popTarget()
popTarget 在讲dep的时候也说了,实际上就是把 Dep.target 恢复成上一个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。
最后执行:
this.cleanupDeps()
在这里我们详细分析下cleanupDeps
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
考虑到 vue 是数据驱动的,所以每次数据变化都会重新 render,但是并不会再重新创建watcher(因为渲染watcher只要在第一次挂载的时候会创建),那么 vm._render() 方法又会再次执行,并再次触发数据的 getters。所以 Watcher 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组。
在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。
那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。
考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。
因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。
computed watcher依赖收集详细过程
// src/core/instance/state.js
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
// 获取get方法
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 一个computed创建一个Watcher
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
// computedWatcherOptions = { lazy: true }
computedWatcherOptions
)
}
}
// 在vue实例上没有的的属性执行defineComputed方法
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
可以看到,在我们初始化过程中,会将我们在vue中定义好的computed对象进行遍历。对于每个computed key都会创建一个computed watcher。
注意,
computed watcher的get方法并不是像渲染watcher一样在实例化的时候执行,而是会延迟执行,这个我们在Watcher的构造函数内也能发现。// src/core/observer/watcher.js // computed watcher lazy会是true,所以第一次不会计算 this.value = this.lazy ? undefined : this.get()
并在后面会判断,在vue实例上没有该属性的时候会执行defineComputed方法。
接下来我们来看看defineComputed方法。
// src/core/instance/state.js
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 浏览器环境 一定是true,也就是会缓存
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
}
// 给当前vue实例定义属性和描述
Object.defineProperty(target, key, sharedPropertyDefinition)
}
因为computed是支持函数写法和对象写法两种方式,所以它这里也进行了判断。
默认情况下,他们都会执行到createComputedGetter方法,也就是给当前computed属性定义get方法。
我们再来看看createComputedGetter方法的逻辑。
// src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// computed watcher dirty的初始值是true
if (watcher.dirty) {
// 真实执行计算属性的get方法
watcher.evaluate()
}
// 依赖收集,在真实渲染的时候才会进来
// 此时Dep.target是渲染watcher
if (Dep.target) {
watcher.depend()
}
// 返回值
return watcher.value
}
}
}
这个方法逻辑就是从当前组件实例的watcher数组中取出watcher。因为computed watcher 的 lazy 是 true ,而Watcher中,this.dirty = this.lazy,所以computed watcher dirty的初始值也是true。
然后执行evaluate()。这里才是真正的去计算值。所以computed是在真实使用到了的情况下(也就是访问到了计算属性的get方法)才会进行计算。否则并不会计算。
接下来我们来看看evaluate()方法
// src/core/observer/watcher.js
evaluate () {
// 执行get获取值,并赋值给当前watcher
this.value = this.get()
// 将标志位置为false,依赖没发生改变就不需要重新计算。
this.dirty = false
}
evaluate是专门给计算属性使用的。首先它会执行get方法获取到计算属性的值,并将值赋值给当前的watcher实例。这也是为什么上面会返回return watcher.value。然后将标志位dirty置为false。也就是计算属性的依赖没发生改变的时候就不需要重新计算了。(也就是我们常说的computed会有缓存功能)。
后面会执行 watcher.depend()进行依赖收集。注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。(其实就是将computed watcher的所有dep又全部添加到渲染watcher的deps中。)
// src/core/observer/watcher.js
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
所以,在computed的依赖发生改变的时候既会通知computed watcher又会通知当前的渲染watcher重新渲染。
当通知computed watcher的时候其实只干了一件事件,就是将标志位又置为true。表示computed在下次使用的时候需要重新计算了。这也是和渲染watcher的另一个区别,在通知更新的时候并不会去重新计算,而只是将标志位置为true。

最后会返回get方法的值。
总的来说就是,一个computed属性对应一个computed watcher,computed watcher的get方法不像渲染watcher一样在实例化的时候执行,而是会延迟,会在真正使用到的时候才会执行。并且在页面渲染的时候,computed的dep也会被同步添加到渲染watcher中来。这样computed的依赖发生改变的时候,页面会重新渲染,计算属性也会重新计算。
并且computed也做了计算优化,在依赖数据没发生改变,也就是标志位没重新恢复到true的时候只会返回之前计算好的值,并不会重新计算。
这就是computed watcher的依赖收集逻辑,小伙伴们是否看懂了呢?
user watcher依赖收集详细过程
// src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
// 数组 遍历
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
这个方法很简单,因为watch的handler可能是数组、对象、字符串、方法,所以在这里进行了初步判断,分别调用createWatcher方法。
// src/core/instance/state.js
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 是对象 就取handler.handler
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 字符串其实就是methods里面的方法名,直接从vm上获取
if (typeof handler === 'string') {
handler = vm[handler]
}
// 其实就是调用 vm.$watch
return vm.$watch(expOrFn, handler, options)
}
在createWatcher中,进行了更深一层的判断,分别对对象、字符串形式的写法做了一下适配。最后调用了我们熟悉的$watch方法。
接下里我们再来看看$watch方法。
// src/core/instance/state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 因为我们可以直接调用$watch,所以这里又进行了判断
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// user wathcer的 user 选项会是 true
options.user = true
// 实例化watcher,它是有回到函数cb的
const watcher = new Watcher(vm, expOrFn, cb, options)
// 配置了immediate会立即执行
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
// 返回取消监听的函数
return function unwatchFn () {
watcher.teardown()
}
}
因为我们可以直接调用$watch,所以这个方法首先会对回调函数再次进行判断。
然后再创建Watcher对象。并且可以发现 user watcher 和 渲染watcher和computed watcher的区别,它是有个回调函数的。
因为user watcher的expOrFn一般会是一个表达式,所以在Watcher实例化的时候会有如下逻辑,会进行转化,转成一个函数。(这是和渲染watcher的第一个区别)

其实对于user watcher我们并不关心它的get,而是它的回调,因为user watcher就是在依赖数据发生改变的时候能执行回调就可以了。所以我们再看看更新方法。

可以发现,它的流程和渲染watcher其实是差不多的,就是最后多了一个回调函数执行的操作。(这是和渲染watcher的第二个区别)
然后对于配置了immediate选项的话会立即执行。
最后返回取消监听的函数。
这就是user watcher依赖收集的总体过程,大体上和渲染watcher还是很像的。
总结
依赖收集主要是通过渲染、computed、user三种watcher进行收集的。
-
render()时,也就是通过渲染函数生成虚拟dom时,触发render watcher依赖收集。 -
在
initState方法中,对computed属性初始化时,触发computed watcher依赖收集。 -
在
initState方法中,对侦听属性初始化时,触发user watcher依赖收集(这里就是我们常写的那个watch)。
所以我们也可以得出,定义在data函数里面的属性一定会被响应式处理。但是不一定会被依赖收集。因为只有在被模板或者computed、watch里面使用到了的属性才会触发get方法被依赖收集。
好啦,关于依赖收集,笔者就讲到这里啦。关于派发更新咱们后面见。😃
扩展
为什么有两种dep在收集依赖?
细心的同学会发现除了在defineReactive方法中,为每个属性都创建了一个dep对象外,其实在new Observer()的时候也创建了一个dep对象。


那为什么会有两个dep对象呢?
对于defineReactive里面的dep对象,我们很清楚,在obj属性的get方法中收集当前watcher,在触发obj属性的set方法的时候通知watcher更新。
但是对于new Observer()里面的dep对象就有点没弄明白了。为什么这里还会有一个dep对象呢?
其实这个dep是给Vue.set/Vue.delete和调用数组七个原型方法,去派发更新时使用的。
我们知道,defineReactive里面的dep对象其实是一个闭包属性,在这个方法里面能访问到,但是在外部是访问不到的。前面我们分析了Vue.set/Vue.delete的原理和数组修改七个原型方法的原理,他们在最后都会有一个手动派发更新的操作。


因为闭包里面的dep对象在外面获取不到,但是这里手动派发更新又需要用到dep,所以就有了new Observer()里面的dep。Vue.set/Vue.delete和数组重写原型方法里面的ob.dep就是new Observer()里面的dep对象。因为他们在依赖收集的时候收集的是同样的watcher,所以用这个dep对象进行派发更新也是可以的。
dep和watcher为什么是双向添加呢?
dep里面添加watcher我们知道,是为了数据修改能方便通知watcher去更新。
但是watcher里面又添加dep是什么意思呢?
其实就是为了方便dep移除subs数组中的watcher。因为我们依赖收集只做了添加,并没有移除,移除其实就是在watcher中判断并处理的。
系列文章
vue2源码分析-data、props、methods、computed属性可以重名吗
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
转载自:https://juejin.cn/post/7155401509797593102