【问题标题】:Trying to understand this Vue reactivity inconsistency试图理解这种 Vue 反应性不一致
【发布时间】:2021-08-04 18:02:20
【问题描述】:

我很困惑...鉴于以下两个示例:

示例 1:https://jsfiddle.net/luckylooke/rbzwme91/

new Vue({
  el: "#app",
  data: {
    message: "Hello!",
    myPropInternal: 0,
  },
  computed: {
    myProp: {
      // getter
      get: function () {
      console.log('get', this.myPropInternal);
      return this.myPropInternal;
      },
      // setter
      set: function (value) {
      console.log('set', value);
      this.myPropInternal = value;
      }
    }
  },
  created: function() {
    this.myProp = 1;
    this.myProp = 2;
    this.myProp = 3;
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
   <h1>{{ message }} - {{ myProp }}</h1>
</div>

示例 2:https://jsfiddle.net/luckylooke/9khzv7a1/8/

const myObject = {
    myPropInternal: 0,
  get myProp() {
      console.log('get', this.myPropInternal);
      return this.myPropInternal;
  },
  set myProp(value) {
      console.log('set', value);
      this.myPropInternal = value;
  }
};

new Vue({
  el: "#app",
  data: {
    message: "Hello!",
    myObject
  },
  created: function() {
    this.myObject.myProp = 1;
    this.myObject.myProp = 2;
    this.myObject.myProp = 3;
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
   <h1>{{ message }} - {{ myObject.myProp }}</h1>
</div>

示例 1 导致此控制台日志:

"set", 1
"set", 2
"set", 3
"get", 3

我希望模型的每次更改都会导致计算值(getter)进行评估,就像在示例 2 中一样,其中的输出是:

"get", 0
"get", 0
"set", 1
"get", 1
"set", 2
"get", 2
"set", 3
"get", 3

为什么 Vue 没有对第一个示例中的每个更改立即做出反应?

【问题讨论】:

  • 计算被优化为不会被相同的值触发。
  • 我得说我 100% 忘记了 computed setters 的存在。
  • @EstusFlask 我同意,但 1,2,3 不一样 ;)
  • iirc 这个是vue优化的,我会努力找源码的。
  • 即使它是一种优化,它也会在错误的地方进行优化,因为第二个示例正在绕过它:/

标签: vue.js vue-reactivity


【解决方案1】:

这就是这里发生的事情,以我从很棒的 Vue documentation 那里收集到的信息、一些有用的研究和一些快速的实验为后盾。

您所说的“反应性差异”的症结在于 Vue 如何使 data 中的属性具有反应性,以及 Vue 如何处理 computed 属性data 中的属性不同 .

Vue 反应性


首先,让我们确定 Vue 如何使 data 属性反应,这与示例 #2 直接相关。

当你在 Vue 中为 data 添加一个属性时,Vue 在渲染之前会做一些预处理工作以使这个属性反应。我不会在这里深入探讨这个过程,但要点是 Vue 在内部用一系列 getter 和 setter 替换你的数据属性。这些 getter 和 setter 允许 Vue 跟踪 什么 数据更改/被访问,何时 它更改/被访问,以及 在什么上下文 它正在更改或在 (this is how computed properties discover their reactive dependencies and know they're stale) 中访问。当相关依赖项发生变化时,Vue 会使用这些数据来相应地更新 DOM。

例如,如果您在模板中有&lt;div&gt;{{ str }}&lt;/div&gt; 作为节点,在data 中有一个str 字段,基于Vue 在渲染之前所做的处理,当str 的值发生变化时,Vue 将检测到此更改并触发 DOM 更新,并最终将更改/新节点修补到 DOM 中,更新您在页面上看到的内容。

可以在on the Vue docs 找到有关此过程的更多信息,我发现this SO answer 也是一个不错的概要。

计算属性


计算属性不像data 属性那样“反应性”,因为computed 属性不存储数据。它们更类似于观察者,可以被认为是在需要时自动触发的方法。

我最想在这里强调computed 属性和data 属性之间的区别是computed 属性仅在必要时重新计算。换句话说,它们只有在被调用时才会重新计算,要么在方法/生命周期挂钩/其他属性中手动进行,要么作为 DOM 中的依赖项(例如&lt;div&gt;{{ computedProp }}&lt;/div&gt;)。如果您有一个在组件脚本中完全未使用且在渲染 DOM 中未引用的计算 prop,则它永远不会运行,即使它的依赖项发生了变化。

这解释了您的示例之间的差异,因为您不是直接更改 data 道具,而是通过 computed 道具代理更改,直到它需要时才会运行它的 getter 函数——在这种情况下,这就是组件最终挂载的时候,并且计算的 prop 被 DOM 中的一个节点引用。

所以直接回答你的问题:

为什么 Vue 没有对第一个示例中的每个更改立即做出反应?

它没有反应,因为没有什么可以反应——它在你调用 setter 时存储值,但 Vue 没有运行 getter,因为没有任何东西主动获取计算属性。

那么为什么data Object Setter 会自动触发 Getter?


那么,我剩下的问题就是为什么示例 #2 会以它的方式工作。在标准 JavaScript 中,运行 setter 不会触发 getter 执行,这是设计使然,因为 getter 和 setter 只是绑定到函数的对象属性:

var obj = {
  log: ['a', 'b', 'c'],
  get last() {
    console.log('Ran Getter, val:', this.log[this.log.length - 1]);
    return this.log[this.log.length - 1];
  },
  set last(x) {
    console.log('Ran Setter, val:', x);
    return this.log[this.log.length - 1] = x;
  },
};

obj.last;        // Trigger Getter
obj.last = 'z';  // Trigger Setter
obj.last;        // Trigger Getter
obj.last;        // Trigger Getter
obj.last = 'y';  // Trigger Setter
obj.last = 'w';  // Trigger Setter

因此,我认为您在 Vue 的 data 的对象中定义的 getter/setter 对所看到的行为实际上是 Vue 反应性方案的副产品。 This Stack Overflow answer 介绍了 Vue 处理预先存在的 getter 的本质,所以我想这就是 Vue 处理它们的反应方式,通过强制 getter 在它们的依赖关系发生变化时重新计算,而不是在它们被调用时进行。

结论


老实说,如果 data 中的 getter 和 setter 不是明确有用的,我建议避免它们,因为这会大大增加复杂性和潜在的意外行为。您可以将 getter 移动到 computed 并将 setter 移动到 methods,它们本质上是组件级别上 getter 和 setter 的 Vue 等价物。如果您将 get/set 功能拆分到组件中,您的组件几乎肯定会在未来更容易理解和调试!


还想注意:您发现您可以强制 getter 使用 await this.$nextTick(); 运行,但在每次分配之前也可以简单地引用计算属性,并产生与示例 #2 相同的日志输出:

new Vue({
  el: "#app",
  data: {
    message: "Hello!",
    myPropInternal: 0,
  },
  computed: {
    myProp: {
      // getter
      get: function() {
        console.log('get', this.myPropInternal);
        return this.myPropInternal;
      },
      // setter
      set: function(value) {
        console.log('set', value);
        this.myPropInternal = value;
      }
    }
  },
  created: function() {
    this.myProp; // Simply referencing the property activates the getter
    this.myProp = 1;
    this.myProp;
    this.myProp = 2;
    this.myProp;
    this.myProp = 3;
  },
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
  <h1>{{ message }} - {{ myProp }}</h1>
</div>

【讨论】:

  • 漂亮的答案,谢谢!我知道优化保护 DOM 被重新渲染,但我以某种方式期望它只会在被模板读取或在中间的某个级别时被优化。这个问题是从应用程序中高度提取的,我需要检查我是否没有进行错误的提取。我稍后会做。但至少我得到了一些新知识,我感谢你。 ?
【解决方案2】:

第一个示例是标准 Vue 计算的,它按预期工作。

Vue 计算点是为了最小化函数(计算属性的getter)需要执行的次数。因此,无论何时执行 setter,Vue 只是将当前值(之前由 getter 返回的)标记为脏,但不会立即执行 getter!因为不需要!下次有人(任何人!不仅仅是模板/渲染函数)尝试读取值时,getter 就会被执行......

现在来看第二个例子。首先请注意第一个示例中如何定义计算道具。您正在给 Vue 一个具有两个属性的对象 - getset。您没有定义新属性(在JavaScript language sense 中)。 Vue 自己做,只需使用你的函数和它自己的代码来创建上述行为......

但在第二个示例中,您使用 JS 构造来创建具有预定义 getter 和 setter 的新对象属性。并将该对象用作 Vue 组件内的响应数据。这与 Vue 计算属性非常不同

【讨论】:

    【解决方案3】:

    我的同事用这个例子玩了一下,发现如果你把 $nextTick() 放在设置值之间,你会得到想要的结果

    
      created: async function() {
        this.myProp = 1;
        await this.$nextTick();
        this.myProp = 2;
        await this.$nextTick();
        this.myProp = 3;
      }
    

    可能是为了优化重绘,但如果是这样,恕我直言,它是在错误的层上完成的。因为我甚至可以为内部控制器逻辑使用/重用计算值,然后不需要这种优化。

    因此可以改为在结果渲染功能中进行优化。甚至 Example2 也表明它是在错误的层上完成的,因为它绕过了这个优化。

    【讨论】:

    • 我不确定我是否同意这种优化是“不受欢迎的”,因为它正在执行 zero 重绘——你在 the created lifecycle hook, which executes fully before render 中调用它。组件在 beforeMountmounted 钩子 (Vue Lifecycle Diagram) 之间才会呈现。
    • 我还不能挖掘源代码,但我认为有问题的行为是有道理的,因为 Vue 在实际呈现之前不需要调用计算的 setter模板。我不确定的是为什么它确实调用setter,如果它只是在对象/数据本身中。
    • 好的,但是仍然存在不一致,因为它在第二个示例中没有优化,当我将一些第三方对象放入数据并且不知道它的实现时,它可能会导致很难检测到的错误:/
    猜你喜欢
    • 2021-05-15
    • 2015-06-30
    • 1970-01-01
    • 2019-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-11
    • 2019-09-06
    相关资源
    最近更新 更多