首先需要先明确一下自己想要做什么?它在哪里做的?它是怎么做的?我该怎么做?在了解这几点以后,就可以开始动手了。
我想做什么?
在重构公司前同事代码的时候,我发现在 vue@2
中有很多的 watcher
,但在实际业务中,有一些条件是互斥的,比如存在 a
属性后,b
属性就不可能存在,那我就不需要去监听 b
属性的变化了,所以我想要去主动取消在 options API
中 watch
选项中定义的 watcher
。
它在哪里做的?
在明确目的以后,就需要意识到,Vue
是怎么取消组件的 watcher
的。在不了解 Vue 源码的情况下,就需要去了解一下 Vue 组件的创建、执行、挂载、更新和销毁流程,也就是生命周期。这里引用 Vue.js 官网上的生命周期流程图: 从图中可以看到,Vue 对
watch
和对子组件 event
监听的解绑操作在 beforeDestroy
之后,destroyed
之前,有了目标以后,我们就可以在源码中找到其关于生命周期的处理函数的位置了:vue/src/core/instance/lifecycle.js
。文件位置是这个,接下来只需要找到 beforeDestroy
钩子调用的地方,在这个文件的 :102
行有这么一个语句:callHook(vm, 'beforeDestroy')
。
它是怎么做的?
继续在源码中查找关于 watch
的字样,直到找到:
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
这一段就是取消所有 watcher
的监听。
我该怎么做?
为了验证一下刚找到的这一段代码是否正确,那么我们可以拷贝源码到本地,同时找到在源码中找到的那一段代码的位置,添加一点调试语句,debugger
console.log
或其他的都可以,再写一段代码用于测试。
复制 CDN 链接 里的代码到本地,方便调试,新建一个 html
文件,写入了类似如下的测试代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
<!-- 下载到本地的 vue 源码 -->
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
count: 0,
message: 'Hello Vue!'
},
template: `<button @click="count++">{{ message }}{{ count }}</button>`,
watch: {
message: function (val, old) {
console.log({ val, old })
}
}
})
vm.message = 'Hello World!'
</script>
</body>
</html>
运行、访问这个文件,当页面中出现了一个按钮后则表示这个实例创建成功。那么就可以开始后续的操作。
代码正常运行后,在浏览器控制台就会有一行 watcher
的打印信息,包含了 val``old
值。尝试打印一下 console.log(vm._watcher, vm._watchers)
是什么。大致结果如下:
Watcher {vm: Vue, deep: false, user: false, lazy: false, sync: false, …}
active: true,
before: ƒ before(),
cb: ƒ noop(a, b, c),
deep: false,
depIds: Set(2) {4, 3},
deps: (2) [Dep, Dep],
dirty: false,
expression: "function () {\n vm._update(vm._render(), hydrating)\n }",
getter: ƒ (),
id: 2,
lazy: false,
newDepIds: Set(0) {size: 0},
newDeps: [],
sync: false,
user: false,
value: undefined,
vm: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
[[Prototype]]: Object
[Watcher, Watcher]
一个组件实例中,至少会存在1个 watcher
, 它用于更新视图,而在 watch
配置项中定义的观察者就存在于 _watchers
中。那我们要做的就是找到目标watcher
,并结束它的监听。我们可以尝试一下源码中的操作,看那段源码做了什么事情:
if (vm._watcher) {
vm._watcher.teardown()
}
var i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
将这段代码放到测试代码中(放在 vm.message = 'Hello World!'
前),刷新页面后就能发现,数据的变化不再能触发视图的更新了,在浏览器中访问 vm
组件实例,查找 count
message
属性,会发现它的值是变化了的。所以可以确定,这段代码就是取消监听的操作。_watcher
属性我们已经看过了,没有眼熟的字段,接下来再去查看 _watchers
。它是一个数组,数组的第二项是 _watcher
属性,还是用于更新视图的,展开第一个watcher
:
0: Watcher
active: true,
before: undefined,
cb: ƒ (val, old),
deep: false,
depIds: Set(1) {4},
deps: [Dep],
dirty: false,
expression: "message",
getter: ƒ (obj),
id: 1,
lazy: false,
newDepIds: Set(0) {size: 0},
newDeps: [],
sync: false,
user: true,
value: "Hello World!",
vm: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
[[Prototype]]: Object
看到一个比较熟悉的字段了:expression
,它的值是 message
。我们尝试去修改一下 watch
属性中的 message
键名为:this.message
,再刷新查看第一个 watcher
中的 expression
字段会发现也已经同步变成了 this.message
,那么基本可以肯定,这个watcher
就是和 message
属性绑定的观察者。 在 watcher
的原型上能找到 teardown
方法,它的作用就是取消监听。
那么这一次学习的目的已经达到了,但是每次使用都需要去手动找显然效率不太高,我们可以去封装一个 unwatch
函数来尝试解决这个问题:
/**
* unwatch watcher
* @param ctx {Vue} vue instance context
* @param key {string} watcher expression
* @returns whether to successfully execute
*/
function unwatch(ctx, key) {
const watcher = ctx._watchers.find(({ expression }) => expression === key)
try {
watcher.teardown()
return true
} catch (e) {
console.warn(e)
return false
}
}
以上。