Libon

Vue2 OptionsAPI unwatched

#vue
学习 Vue2 中怎么主动取消在 watch 监听回调函数

ToC

我想做什么?

在重构公司前同事代码的时候,我发现在 vue@2 中有很多的 watcher,但在实际业务中,有一些条件是互斥的,比如存在 a 属性后,b 属性就不可能存在,那我就不需要去监听 b 属性的变化了,所以我想要去主动取消在 options APIwatch 选项中定义的 watcher

它在哪里做的?

在明确目的以后,就需要意识到,Vue是怎么取消组件的 watcher的。在不了解 Vue 源码的情况下,就需要去了解一下 Vue 组件的创建、执行、挂载、更新和销毁流程,也就是生命周期。这里引用 Vue.js 官网上的生命周期流程图: image.png 从图中可以看到,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 链接 里的代码到本地,方便调试。我这里用的 vue 版本是 2.6.14,新建一个 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]

一个组件实例中,至少会存在1watcher, 它用于更新视图,而在 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
  }
}

调用时只需要传入 vue实例和 watch属性中的键名即可:

unwatch(this, 'message')

在点击 unwatch 按钮以后再点击 Add 按钮后控制台将不会有任何输出, 完整的示例代码如下:

<script>
function unwatch(ctx, key) {
  const watcher = ctx._watchers.find(({ expression }) => expression === key)

  try {
    watcher.teardown()
    return true
  } catch (e) {
    console.warn(e)
    return false
  }
}


export default {
  data() {
    return {
      count: 1
    }
  },

  watch: {
    count: {
      deep: true,
      handler(newValue, oldValue) {
        console.log(newValue, oldValue)
      }
    }
  },

  methods: {
    unwatchCount() {
      unwatch(this, 'count')
    }
  }
}
</script>

<template>
  <div class="greetings">
    <h1 class="green">{{ count }}</h1>
    <h3>
      <button @click="count++">Add</button>
    </h3>

    <button @click="unwatchCount">unwatch</button>
  </div>
</template>

2024-01-11 更新

我注意到在 Vue2.7.0 以后的版本因为增加了 setup 语法的原因,重写了一部分编译相关的内容,已经不存在 _watchers 属性了,所以以上的代码需要进行一些调整,兼容一下高版本的 vue,所以 unwatch 代码更新如下:

function unwatch(ctx, key) {
  const watcher = (ctx._watchers || ctx._scope.effects).find(({ expression }) => expression === key)

  try {
    watcher.teardown()
    return true
  } catch (e) {
    console.warn(e)
    return false
  }
}

以上。


CD ..