反应性是状态和 DOM 之间的自动同步。这就是 Vue 和 React 等视图库在其核心中尝试做的事情。他们以自己的方式做到这一点。
我认为 Vue 的反应系统有两个方面。一方面是 DOM 更新机制。让我们先研究一下。
假设您有一个带有如下模板的组件:
<template>
<div>{{ foo }}</div>
</template>
<script>
export default {
data() {
return {foo: 'bar'};
}
}
</script>
这个模板被转换成渲染函数。这发生在使用vue-loader 的构建期间。上面模板的渲染函数类似于:
function anonymous(
) {
with(this){return _c('div',[_v(_s(foo))])}
}
渲染函数在浏览器上运行,执行时返回一个 Vnode(虚拟节点)。虚拟节点只是代表实际 DOM 节点的简单 JavaScript 对象,是 DOM 节点的蓝图。上面的渲染函数在执行时会返回类似:
{
tag: 'div',
children: ['bar']
}
Vue 然后从这个 Vnode 蓝图创建实际的 DOM 节点并将其放入 DOM 中。
稍后,假设foo 的值发生了变化,不知何故渲染函数再次运行。它将给出不同的 Vnode。然后 Vue 将新 Vnode 与旧 Vnode 进行区分,并仅修补 DOM 所需的必要更改。
这为我们提供了一种有效更新 DOM 的机制,以获取组件的最新状态。如果每次组件的任何状态(数据、道具等)发生变化时都会调用组件的渲染函数,那么我们就有了完整的反应系统。
这就是 Vue 反应式硬币的另一面。那就是反应式 getter 和 setter。
如果您还没有意识到这一点,这将是了解 Object.defineProperty API 的好时机。因为 Vue 的响应式系统依赖于这个 API。
TLDR;它允许我们使用自己的 getter 和 setter 函数覆盖对象的属性访问和赋值。
当 Vue 实例化您的组件时,它会遍历您的 data 和 props 的所有属性,并使用 Object.defineProperty 重新定义它们。
它实际上做的是,它defines getters and setters 用于每个 data 和 props 属性。通过这样做,它会覆盖该属性的点访问 (this.data.foo) 和赋值 (this.data.foo = someNewValue)。因此,只要在该属性上发生这两个操作,就会调用我们的覆盖。所以我们有一个钩子可以对它们做点什么。我们稍后再讨论这个问题。
此外,为每个属性创建一个new Dep() 类实例。之所以称为Dep,是因为每个 data 或 props 属性都可以是组件渲染函数的dependency。
但首先,重要的是要知道每个组件的渲染函数都会被调用within a watcher。所以观察者有一个关联的组件的渲染函数。 Watcher 也用于其他目的,但是当它正在监视组件的渲染函数时,它是render watcher。观察者将自己分配为current running watcher,全局可访问的某个地方(在Dep.target 静态属性中),然后运行组件的render function。
现在我们回到反应式 getter 和 setter。当您运行渲染函数时,会访问状态属性。例如。 this.data.foo。这会调用我们的 getter 覆盖。当 getter 被调用时,dep.depend() 被调用。这会检查是否在Dep.target 中分配了当前正在运行的观察者,如果有,则将该观察者分配为此 dep 对象的订阅者。之所以称为dep.depend(),是因为我们让watcher 依赖于dep。
_______________ _______________
| | | |
| | subscribes to | |
| Watcher | --------------> | Dep |
| | | |
|_____________| |_____________|
与
相同
_______________ _______________
| | | |
| Component | subscribes to | it's |
| render | --------------> | state |
| function | | property |
|_____________| |_____________|
稍后,当 state 属性被更新时,setter 被调用并且相关的 dep 对象通知它的订阅者新的值。订阅者是可以感知渲染函数的观察者,这就是组件渲染函数在其状态发生变化时自动调用的方式。
这使得反应系统完整。我们有办法在组件状态发生变化时调用组件的渲染函数。一旦发生这种情况,我们就有办法有效地更新 DOM。
通过这种方式,Vue 创建了状态属性和渲染函数之间的关系。当状态属性发生变化时,Vue 确切地知道要执行哪个渲染函数。这可以很好地扩展,并且基本上从开发人员手中消除了一类性能优化责任。无论组件树有多大,开发人员都无需担心组件的过度渲染。为了防止这种情况,React 例如提供 PureComponent 或 shouldComponentUpdate。在 Vue 中,这不是必需的,因为 Vue 知道在任何状态发生变化时要重新渲染哪个组件。
但是现在我们知道了 Vue 如何让事情发生反应,我们可以想办法稍微优化一下事情。假设您有一个博客文章组件。您从后端获取一些数据并使用 Vue 组件将它们显示在浏览器上。但是博客数据没有必要是被动的,因为它很可能不会改变。在这种情况下,我们可以通过冻结对象来告诉 Vue 跳过对此类数据进行响应式处理。
export default {
data: () => ({
list: {}
}),
async created() {
const list = await this.someHttpClient.get('/some-list');
this.list = Object.freeze(list);
}
};
Oject.freeze 除其他外,禁用对象的可配置性。您不能使用Object.defineProperty 再次重新定义该对象的属性。所以 Vue skips 整个反应性设置都适用于这些对象。
此外,自己浏览 Vue 源代码,还有两个关于这个主题的非常好的资源:
- Vue Mastery 的Advanced component 课程
- Evan You 的 FrontendMaster 的 Advanced Vue.js Features from the Ground Up
如果您对简单虚拟 DOM 实现的内部结构感到好奇,请查看 Jason Yu 的博文。
Building a Simple Virtual DOM from Scratch