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 + transportIdcarId * transportId

据我所知,Vue 出现在我的视野中,看着它们,无论我有 getter(carId+ transportIdcarId * 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 反应性如何在幕后工作?的主要内容,如果未能解决你的问题,请参考以下文章

Meteor 的反应在幕后是如何工作的?

从头开始创建自己的Vue.js—第2部分(虚拟DOM基础)

如何更新反应性对象(和一般属性)?

Vue.js 3 - 替换/更新反应性对象而不会失去反应性

使用 Vue.js 跨浏览器选项卡的反应式 localStorage 对象

在 Vue.js 中反应性地重新填充指令数组