Vue.js 反应性如何在幕后工作?
Posted
技术标签:
【中文标题】Vue.js 反应性如何在幕后工作?【英文标题】:How does Vue.js reactivity work under the hood? 【发布时间】:2019-06-25 20:39:25 【问题描述】:基本上,当我有一个组件时,我们称之为“TransportComponenet.vue”,在那个组件中,我有一个 data(),我的属性是 carId、transportId。 vue 所做的是为这些属性创建 getter 和 setter。假设在这个组件的视图中,我输入carId + transportId
和carId * transportId
。
据我所知,Vue 出现在我的视野中,看着它们,无论我有 getter(carId+ transportId
或 carId * transportId
)哪里都是 getter。所以 vue 来了,并将它们注册到组件的 watcher 中。当我在某个地方使用诸如this.carId = 5
之类的设置器时。 Vue 对此属性执行 setter 函数并重新评估之前保存在 watcher 中的函数(getter)。这是正确的假设吗?
我不明白 Dep 类和 Watcher 类之间存在什么关系?我知道他们都扮演着重要的角色。我真的很尊重整个解释“哪件事发生在哪里、何时以及为什么”。
【问题讨论】:
vuemastery.com/courses/advanced-components/…有一个很棒的教程 【参考方案1】:反应性是状态和 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
【讨论】:
以上是关于Vue.js 反应性如何在幕后工作?的主要内容,如果未能解决你的问题,请参考以下文章