持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 32 天,点击查看活动详情
Vue2中-Dep 和 Watcher 之间的联系
start
- 初次查看 Vue2 源码中 Dep 和 Watcher 相关的源码,发现它们互相存储,互相调用。
- 第一次阅读是没有理解透彻它们之间的联系,今天再次阅读,豁然开朗,写一个文章记录一下。
正文
- 首先我们写一个基础的
Html
案例,代码如下:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>lazy_tomato</title>
</head>
<body>
<div id="app">
<div>{{ tomato }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
tomato: '番茄',
}
},
})
</script>
</body>
</html>
-
结合上述的案例,梳理一下代码执行逻辑:
首先以 cdn 的方式引入了一个完整版 Vue.js,写了一个 Vue2 的基础案例。
运行 JS 的时候,会执行
new Vue()
,页面开始挂载$mount
。$mount
中会执行new Watcher()
(即创建一个渲染 watcher,标记它为w1
)。然后在
new Watcher()
的过程中会依次执行下面的逻辑:- 首先在
Dep.target
上存储当前的this
(也就是w1
);Dep
是一个类,用Dep.target
存储w1
,保证在全局环境同一时刻,Dep.target
上存储的是w1
。(方便其他函数获取); - 随后执行
updateComponent
,这个函数会创建虚拟DOM
+渲染页面
。由于页面中使用了data
中的tomato
属性,随即会触发tomato
的getter
; updateComponent
执行完毕后,然后会调用w1.cleanupDeps
方法;(这个方法后续会讲)
上述逻辑代码如下:
// 精简过后的代码 let updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, true)
- 首先在
-
理解一下触发属性
tomato
的getter
逻辑:对于属性
tomato
,会有一个专门收集它相关依赖的 dep (暂记为d1
)。上述的 dep 是闭包中定义的 dep。
当触发属性
tomato
的getter
,就会触发dep.depend
(也就是d1.depend
)。tomato 的 getter
var dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { if (Dep.target) { dep.depend(); } return value; }, }
dep.depend
函数中会触发Dep.target.addDep(this);
,Dep.target
上存储的是当前Watcher实例
(w1
),所以等同于w1.addDep(d1)
(此时的 this 指向当前的 dep,也就是d1
)。dep.depend
Dep.prototype.depend = function depend() { if (Dep.target) { Dep.target.addDep(this) // Dep.target.addDep(this) 可以理解为 `w1.addDep(dep)` } }
-
在看
w1.addDep(d1)
的具体逻辑之前,先介绍一下Watcher实例
(w1
) 上的四个变量:deps = [];
存储旧 dep 的数组depIds = new Set();
存储去重后的 旧 dep 的 idnewDeps = [];
存储新 dep 的数组newDepIds = new Set();
去重 新 dep 容器
deps , depIds
存储旧 dep 的相关信息;newDeps , newDepIds
存储新 dep 的相关信息。新旧 dep 是什么概念? 当页面发生改变触发 getter,
旧dep
存储上次的依赖;新dep
存储最新的依赖。其中数组类型的变量(
deps , newDeps
)是用来存储真实的 dep 对象Set 类型的变量(
depIds , newDepIds
)是用来存储去重后的 dep 的 id。 (使用 Set 类型的数据目的是为了去重)首次渲染的时候,
旧dep
相关的变量为空。 -
w1.addDep
的逻辑:如果
w1.newDepIds
中没有存储d1的id
,w1的新dep
中存储d1
。如果w1的新旧dep
都没有d1的id
,d1
存储当前的Watcher实例
(w1
)。这个地方可能有点绕,对照下方的代码,我解释一下:
-
逻辑的起点是
dep.depend
d1.depend
-
Dep.target
上存储的当前Watcher实例
存储dep
w1上存储d1
-
如果
Watcher实例
从来就没有存储过我这个 dep, 我这个 dep 存储一下当前的Watcher实例
d1上存储w1
这个逻辑过滤下来就实现了:
Watcher实例
中存储Dep实例
;Dep实例
中存储了Watcher实例
;w1.addDep
Watcher.prototype.addDep = function addDep(dep) { // dep => d1 // this => w1 var id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } }
-
-
再说说
cleanupDeps
在
Watcher实例
上,如果旧dep
存在新dep
不存在。(说明之前有依赖过,最新的没有依赖,1.自身不会存储旧的 dep;2.通知对应 dep 删除我这个 Watcher 实例)Watcher.prototype.cleanupDeps = function cleanupDeps() { var i = this.deps.length // 遍历旧的dep,如果新的dep中没有对应id的dep,说明旧的dep项不需要依赖了, 旧的dep项删除我这个watcher while (i--) { var dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } // 这一次逻辑执行完毕了,更新旧的dep为此次手机的最新的dep, 新dep数组清空,供下次使用。 var 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 }
提问
1.dep 中存储的 watcher 有什么用?
当我的数据改变的时候,也就是触发了数据
setter
的时候。遍历 dep 中存储的每一个wathcer实例
,并调用watcher实例
上的update
方法通知更新;
2.watcher 中存储 dep 有什么用?
- 去重,可以避免重复的向 dep 中 push 同一个 watcher。(对应函数
Watcher.prototype.addDep
;当然去重的逻辑也可以放在 Dep 中处理)- 可以对无用的 dep 进行清除,可以对 dep 中无用的 watcher 进行清除。 (对应函数
cleanupDeps
;就比如上次渲染有一个 watcher 观察了一个数据,最新的页面,没有关注这个数据了,1.Watcher 中的newDepIds,newDeps
不收集旧的 dep;2.通知 dep 中去除当前的 watcher)- 当页面销毁的时候,告诉所有存储了我这个
watcher实例
的 dep,删除我这个 watcher。(对应函数Vue.prototype.$destroy
,组件销毁的时候,会遍历组件上的_watcher
属性,然后调用Watcher.prototype.teardown
方法,遍历watcher.deps
, 删除对应 dep 中对应的watcher实例
。
Watcher
上的 _watcher
属性
上面提到了 Watcher
上的 _watcher
属性,介绍一下它。
代码:
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
}
每个组件的上都有 _watcher
属性,它存储的是这个组件中所有的 watcher实例
。例如渲染watcher,计算属性,用户自定义的watcher。
这个属性的作用就是,在组件销毁的时候,找到这个组件的所有 watcher实例
,告诉对应的 dep,取消对对应watcher实例的收集。
end
好啦,用这么多篇幅,介绍了 Dep 和 Watcher 之间的关系。它们两个的实例互相存储,归根到底是为了满足各自的任务。分工明确,既能保证我存储了你,方便能随时找到你,和你说话。又能保证你销毁的时候,你会主动告诉我,你快被销毁了。例子中:我就是dep,你就是 watcher