[译] 监测与调试 Vue.js 的响应式系统:计算属性树(Computed Tree)

关于 Vue 的下一个主版本,公布的不少新特性引起了激烈的讨论,但其中有一个特性引发了个人注意:javascript

更良好的可调试能力:咱们能够精确地追踪到一个组件发生重渲染的触发时机和完成时机,及其缘由前端

在本文中,咱们将讨论在 Vue2.x 中如何监测响应式机制,而且将演示一些和性能调优相关的代码段。vue

为何响应式系统相关代码须要调优

若是你的项目比较大,那么你颇有可能在用 Vuex。你会将 store 分割为模块,而且为了关联数据的访问一致性你甚至须要将你的状态范式化java

你可能使用 Vuex 的 getter 来派生状态,事实上,你还会使用复合的派生数据,即一个 getter 会引用另外一个 getter 派生的数据。node

在 Vue 组件中,你会使用各类分层的模式,固然也包括常常用的 slots。在这样的组件树中,确定会有计算属性(派生出来的数据)。react

当这些发生的时候,从 store 中的状态到渲染的组件之间的响应式依赖关系将很难理清楚。android

这就是计算属性树了,若是不把它弄清楚的话,那么翻转一个看似不起眼的布尔值可能会触发一百个组件的更新。ios

基础知识

咱们将学习一些响应式机制的内部工做原理。若是你尚未(比较深地)理解 Dependency 类(译者注:Dep — 为与源码一致,后文都采用 Dep)与 Watcher 类之间的关系,能够考虑学习一下内容丰富、条例清晰的高级 Vue 课程:创建一个响应式系统git

在浏览器开发工具中调试过程当中见过 __ob__ 么?

认可吧,当时是否是有点好奇,__ob__ 看起来是否是像这样?github

这些在 subs 中的 Watcher 将会在这个响应式数据发生改变的时候更新。

有时候你会在开发者工具中浏览一下这些对象,而且找到一些有用的信息,有时候找不到。有时候你会发现 Watcher 远不止 5 个。

举个例子

咱们用一些简单的代码说明一下:JSFiddle

这个例子的 store 中的状态有散列数组 userscurrentUserId 两个属性。还有一个 getter 用来返回当前用户的信息。另外还有一个 getter 只返回状态为活跃的用户数组。

而后这里有两个组件,其中有三个计算属性:

  • validCurrentUser — 若当前用户是有效用户则为 true
  • total — 引用反映当前全部活跃用户的 getter,将返回活跃用户数
  • upperCaseName — 将用户的姓名映射为大写形式

但愿举的这个特别的例子,对理解咱们讨论的内容有所帮助。

计算属性的响应式机制是如何运转的?

一般,当从一个 Dep 类实例获取到更新的通知时,响应机制将会触发对应的 Watcher 函数。当我变动一个被组件渲染所依赖的响应式数据时,将触发重渲染。

但咱们看看派生的数据,它的状况有点复杂。首先,计算属性的值是被缓存起来的,以便在它计算出来以后就一直可用计算后的值,只有当它的缓存失效才会被从新计算,换句话说,只在其依赖的数据发生改变时它们才会从新求值。

咱们再来看看以前的例子currentUserId 状态被 currentUser 这个 getter 引用了,而后在 validCurrentUser 计算属性引用了 currentUservalidCurrentUser 又是根组件 render 函数的 v-if 表达式的一部分。这条引用链看起来不错。

实际上,响应数据的存储是经过一个 Watcher 的配置选项来处理的。当咱们使用组件中的 Watcher 时,API 文档中介绍了两个可选选项(deepimmediate),但其实还有一些没被文档记录的选项,我并不推介你使用这些没被记录的选项,但理解他们却颇有益处。其中一个选项是 lazy,配置它以后 Watcher 将会维护一个 dirty 标志,若是依赖的响应数据已经更改但这个 Watcher 还未运行时它将为 true,也就是说,此时缓存已过期。

在咱们的例子中,若是 currentUserId 被改为 3。任何依赖于它且被设置了 lazy 的 Watcher 都会被标记为 dirty,但 Watcher 并无运行。currentUservalidCurrentUser 都是这个状态的 lazy Watcher。根渲染函数一样会依赖于这个状态,渲染将在下一个 tick 时被触发。当渲染函数执行时,将会访问已经被标记为 dirty 的 validCurrentUser,它将从新运行它的 getter 函数,进而访问一样须要更新的 currentUser。至此,这个组件将会被正确重渲染,而且相关缓存将被更新。

等等,我彷佛听见你在问,为何全部 3 个 Watcher 都是依赖于这个状态的呢?

难道他们不是相互依赖的么?计算属性 watcher 有一个特性就是不只它自身的值是响应式的,并且当计算属性的 getter 被调用时,若是当前有 Wathcer 在读取这个计算属性的话(即 Dep.target 中有值--译者),全部这个计算属性的依赖也将会被这个 Wathcer 收集起来。这种依赖收集关系链的扁平化对性能表现更优,并且也是个比较简单的解决方案。

这意味着一个组件将发生更新,即便它所依赖的计算属性在从新计算后的值并无发生变化,这种更新显然没有什么意义。

其中一些逻辑能够阅读一下 watcher 类源码的优雅实现,代码量 240 行左右。

那么从 __ob__ 中咱们能够获得哪些关于计算属性响应式机制的信息呢

咱们能够看到有哪些 Watcher 订阅(subs)了响应式数据的更新。记住,响应式机制在下面这些情景下起做用:

  • 对象
  • 数组
  • 对象的属性

最后一个情景颇有可能被忽略,由于在开发者工具中是没法浏览它的 Dep 类实例(译者注:__ob__)。由于 Dep 类是在最初响应式化的时候就被实例化的,可是并无在这个对象中的什么地方把它记录下来。稍后咱们将回头讨论这个问题,由于我将用一个小技巧来间接拿到它。

然而经过观察对象和数组的 Watcher 也可让咱们收获良多,下面是一个简单的 Watcher:

示例跑起来以后打开开发者工具,它应该在页面所有渲染完成以后暂停运行。你能够输入下面的表达式,就能看到跟上面这个图同样的状况了:

this.$store.state.users[2].__ob__.dep.subs[5]
复制代码

这是一个组件的渲染 Watcher,也是一个对象引用。能看到 dirtylazy 这两个我以前提到过的标志位。同时,咱们还能够知道它不是一个用户建立的 Watcher(译者注:user 为 false)。

有时,试图找出这个 Watcher 是哪一个组件的渲染 Watcher 是困难的,由于若是这个组件没有全局注册,或者这个组件没有设置 name 属性,那么基本能够说它是匿名的。然而若是你从另外一个组件引用了这个匿名组件的时候,它的 $vnode.tag 属性一般包含它被引用时所用的名称。

上面的这个 Watcher 来自于被其父组件定义为 Comp 的子组件。它与 upperCaseName 计算属性相关。计算属性一般有一个在 getter 函数上指明的有意义的名称,这是由于计算属性一般被定义为对象属性。

Vuex 的 getter

一般计算属性会给出他们的名称及其所属的组件,可是 Vuex 的 getter 却并不如此。currentUser 这个 Watcher 看起来长这样:

惟一能证实它是 Vuex 中的 getter 的线索是:它的函数体定义在 vuex.min.js 中(译者注:[[FunctionLocation]])。

因此咱们应该怎样获取 getter 的名称呢?在开发者工具中你一般能够访问 [[Scopes]],你能够在 [[Scopes]] 中找到它的名称,然而这并非经过编程的方式来获取的。

下面是个人一个解决方法,在建立 Vuex 的 store 以后运行:

const watchers = store._vm._computedWatchers;
Object.keys(watchers).forEach(key => {
  watchers[key].watcherName = key;
});
复制代码

第一行可能看起来有点奇怪,但其实 Vuex 的 store 中会维护一个 Vue 的实例,来帮助实现 getter 的功能,实际上,getter 就是一个假装起来的计算属性!

如今,当咱们查看 subs 数组中的 Watcher 时,咱们能够经过获取 watcherName 来获取 Vuex 的 getter 的名称。

对象属性的 Dep 类实例

上面我提到调试响应式数据时你是看不到对象属性的 Dep 类实例。

示例中,每一个 user 对象都有一个 name 属性,每一个属性都包含各自的 Watcher,这些 Watcher 将会在属性发生变动时收到更新通知。

尽管 Dep 实例并不能直接访问到,可是能够被监听他们的 Watcher 访问到。Watcher 保留有一份它所依赖的全部依赖项的数组。

个人小技巧是给属性增长一个 Watcher,而后拿到这个 Watcher 的依赖项

可是这并不简单,我能够经过 Vue 的 $watch 接口来添加一个 Watcher,可是返回的并非 Watcher 实例。所以我须要从 Vue 实例的内部属性中获取到 Watcher 实例。

const tempVm = new Vue();
tempVm.$watch(() => store.state.users[2].name, () => {});
const tempWatch = tempVm._watchers[0];

// now pull the subs from the deps
tempWatch.deps.forEach(dep => dep.subs
  .filter(s => s !== tempWatch)
  .forEach(s => subs.add(s)));
复制代码

想把这个功能包装成一个工具函数吗?

我已经把这些小的代码片断封装到了一个任何人均可以获取到的工具库中:vue-pursue

能够看看使用示例

例子中的 () => this.$store.state.users[2].name 通过 vue-pursue 处理后返回:

{
  "computed": [
    "currentUser",
    "validCurrentUser",
    "Comp.upperCaseName"
  ],
  "components": [
    "Comp"
  ],
  "unrecognised": 1
}
复制代码

须要注意的是,根组件将会在操做后更新,但由于根组件没有名称,因此其显示为 unrecognisedcurrentUser 这个 Vuex 的 getter 将会更新,且这个更新并不来源于 name 的更新。

经过传递一个箭头函数给 vue-pursue,这个箭头函数所具备的全部依赖将会被将会被订阅者考虑在内,这意味着 usersusers[2] 对象也包括在内。或者,若是咱们传递 (this.$store.state.users[2], ‘name’),输出将会是:

{
  "computed": [
    "validCurrentUser",
    "Comp.upperCaseName"
  ],
  "components": [
    "Comp"
  ],
  "unrecognised": 1
}
复制代码

最后一点...

我须要着重强调的是,要谨慎使用任何如下划线做为开头的属性,由于这不是公共 API 的一部分,它们可能会在没有任何警告的状况下被移除。上面介绍的这个功能,一开始就没打算使用于生产环境,也没打算使用在运行时环境,这只是一个方便调试的开发者工具。

最终随着 Vue3.0 的出现,这将会被更全面、更简单易用、更可靠的替代。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

PS:欢迎你们关注个人公众号【前端下午茶】,一块儿加油吧~

另外能够加入「前端下午茶交流群」微信群,长按识别下面二维码便可加我好友,备注加群,我拉你入群~

相关文章
相关标签/搜索