ToC
我想做什么?
在重构公司前同事代码的时候,我发现在 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
的字样,直到找到:
1// teardown watchers2if (vm._watcher) {3 vm._watcher.teardown()4}5let i = vm._watchers.length6while (i--) {7 vm._watchers[i].teardown()8}
这一段就是取消所有 watcher
的监听。
我该怎么做?
为了验证一下刚找到的这一段代码是否正确,那么我们可以拷贝源码到本地,同时找到在源码中找到的那一段代码的位置,添加一点调试语句,debugger
console.log
或其他的都可以,再写一段代码用于测试。
复制 CDN 链接 里的代码到本地,方便调试。我这里用的 vue 版本是 2.6.14,新建一个 html
文件,写入了类似如下的测试代码:
1<!DOCTYPE html>2<html lang="en">3
4<head>5 <meta charset="UTF-8" />6</head>7
8<body>9 <div id="app"></div>10 <!-- 下载到本地的 vue 源码 -->24 collapsed lines
11 <script src="./vue.js"></script>12
13 <script>14
15 const vm = new Vue({16 el: '#app',17 data: {18 count: 0,19 message: 'Hello Vue!'20 },21 template: `<button @click="count++">{{ message }}{{ count }}</button>`,22
23 watch: {24 message: function (val, old) {25 console.log({ val, old })26 }27 }28 })29
30 vm.message = 'Hello World!'31 </script>32</body>33
34</html>
运行、访问这个文件,当页面中出现了一个按钮后则表示这个实例创建成功。那么就可以开始后续的操作。
代码正常运行后,在浏览器控制台就会有一行 watcher
的打印信息,包含了 val
old
值。尝试打印一下 console.log(vm._watcher, vm._watchers)
是什么。大致结果如下:
1Watcher {vm: Vue, deep: false, user: false, lazy: false, sync: false, …}2 active: true,3 before: ƒ before(),4 cb: ƒ noop(a, b, c),5 deep: false,6 depIds: Set(2) {4, 3},7 deps: (2) [Dep, Dep],8 dirty: false,9 expression: "function () {\n vm._update(vm._render(), hydrating)\n }",10 getter: ƒ (),11 collapsed lines
11 id: 2,12 lazy: false,13 newDepIds: Set(0) {size: 0},14 newDeps: [],15 sync: false,16 user: false,17 value: undefined,18 vm: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},19 [[Prototype]]: Object20
21[Watcher, Watcher]
一个组件实例中,至少会存在1个 watcher
, 它用于更新视图,而在 watch
配置项中定义的观察者就存在于 _watchers
中。那我们要做的就是找到目标watcher
,并结束它的监听。我们可以尝试一下源码中的操作,看那段源码做了什么事情:
1if (vm._watcher) {2 vm._watcher.teardown()3}4
5var i = vm._watchers.length6while (i--) {7 vm._watchers[i].teardown()8}
将这段代码放到测试代码中(放在 vm.message = 'Hello World!'
前),刷新页面后就能发现,数据的变化不再能触发视图的更新了,在浏览器中访问 vm
组件实例,查找 count
message
属性,会发现它的值是变化了的。所以可以确定,这段代码就是取消监听的操作。_watcher
属性我们已经看过了,没有眼熟的字段,接下来再去查看 _watchers
。它是一个数组,数组的第二项是 _watcher
属性,还是用于更新视图的,展开第一个watcher
:
10: Watcher2 active: true,3 before: undefined,4 cb: ƒ (val, old),5 deep: false,6 depIds: Set(1) {4},7 deps: [Dep],8 dirty: false,9 expression: "message",10 getter: ƒ (obj),9 collapsed lines
11 id: 1,12 lazy: false,13 newDepIds: Set(0) {size: 0},14 newDeps: [],15 sync: false,16 user: true,17 value: "Hello World!",18 vm: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}19 [[Prototype]]: Object
看到一个比较熟悉的字段了:expression
,它的值是 message
。我们尝试去修改一下 watch
属性中的 message
键名为:this.message
,再刷新查看第一个 watcher
中的 expression
字段会发现也已经同步变成了 this.message
,那么基本可以肯定,这个watcher
就是和 message
属性绑定的观察者。 在 watcher
的原型上能找到 teardown
方法,它的作用就是取消监听。
那么这一次学习的目的已经达到了,但是每次使用都需要去手动找显然效率不太高,我们可以去封装一个 unwatch
函数来尝试解决这个问题:
1/**2 * unwatch watcher3 * @param ctx {Vue} vue instance context4 * @param key {string} watcher expression5 * @returns whether to successfully execute6 */7function unwatch(ctx, key) {8 const watcher = ctx._watchers.find(({ expression }) => expression === key)9
10 try {7 collapsed lines
11 watcher.teardown()12 return true13 } catch (e) {14 console.warn(e)15 return false16 }17}
调用时只需要传入 vue
实例和 watch
属性中的键名即可:
1unwatch(this, 'message')
在点击 unwatch
按钮以后再点击 Add
按钮后控制台将不会有任何输出, 完整的示例代码如下:
1<script>2function unwatch(ctx, key) {3 const watcher = ctx._watchers.find(({ expression }) => expression === key)4
5 try {6 watcher.teardown()7 return true8 } catch (e) {9 console.warn(e)10 return false38 collapsed lines
11 }12}13
14
15export default {16 data() {17 return {18 count: 119 }20 },21
22 watch: {23 count: {24 deep: true,25 handler(newValue, oldValue) {26 console.log(newValue, oldValue)27 }28 }29 },30
31 methods: {32 unwatchCount() {33 unwatch(this, 'count')34 }35 }36}37</script>38
39<template>40 <div class="greetings">41 <h1 class="green">{{ count }}</h1>42 <h3>43 <button @click="count++">Add</button>44 </h3>45
46 <button @click="unwatchCount">unwatch</button>47 </div>48</template>
2024-01-11 更新
我注意到在 Vue2.7.0 以后的版本因为增加了 setup 语法的原因,重写了一部分编译相关的内容,已经不存在 _watchers
属性了,所以以上的代码需要进行一些调整,兼容一下高版本的 vue,所以 unwatch
代码更新如下:
1function unwatch(ctx, key) {2 const watcher = (ctx._watchers || ctx._scope.effects).find(({ expression }) => expression === key)3
4 try {5 watcher.teardown()6 return true7 } catch (e) {8 console.warn(e)9 return false10 }1 collapsed line
11}
以上。