首页 >web前端 >js教程 >了解VUE 3中的新反应性系统

了解VUE 3中的新反应性系统

Lisa Kudrow
Lisa Kudrow原创
2025-02-10 08:53:09583浏览

Understanding the New Reactivity System in Vue 3

现代前端框架的核心组成部分之一是响应式系统。它们是让应用实现高交互性、动态性和响应性的魔法棒。理解响应式系统是什么以及如何在实践中应用它,对于每个 Web 开发人员来说都是一项至关重要的技能。

响应式系统是一种机制,它自动地将数据源(模型)与数据表示(视图)层保持同步。每当模型发生变化时,视图都会重新渲染以反映这些变化。

让我们以一个简单的 Markdown 编辑器为例。它通常有两个窗格:一个用于编写 Markdown 代码(修改底层模型),另一个用于预览编译后的 HTML(显示更新后的视图)。当您在编写窗格中编写内容时,它会立即且自动地在预览窗格中预览。当然,这只是一个简单的例子。通常情况要复杂得多。

在许多情况下,我们要显示的数据取决于其他一些数据。在这种情况下,会跟踪依赖项并相应地更新数据。例如,假设我们有一个 fullName 属性,它取决于 firstName 和 lastName 属性。当任何依赖项被修改时,fullName 属性会自动重新计算,结果会显示在视图中。

既然我们已经确定了什么是响应式,那么现在是时候学习新的 Vue 3 响应式机制的工作原理以及如何在实践中使用它了。但在我们这样做之前,我们将快速浏览一下旧的 Vue 2 响应式机制及其缺点。

关键要点

  • Vue 3 引入了一个完全改进的响应式系统,利用 ES6 Proxy 和 Reflect API,增强了灵活性和功能。
  • Vue 3 中的新响应式系统自动跟踪和更新应用程序状态中的更改,支持更复杂的数据结构,如 Map 和 Set。
  • Vue 3 的 reactiverefreadonly 方法允许开发人员更精细地控制数据响应性,其中 ref 用于基本类型,reactive 用于对象。
  • 高级响应式 API 方法(如 computedwatch)为开发人员提供了工具,通过有效地管理依赖项和副作用来创建更动态和响应的应用程序。
  • Vue 3 解决了 Vue 2 响应式系统中发现的限制,例如检测数组长度和对象属性添加的更改。
  • 尽管 Vue 3 响应式系统具有优势,但它仅在 ES6 环境中受支持,并且在身份比较方面,响应式代理和原始对象有所不同。

简要探索 Vue 2 响应式机制

Vue 2 中的响应式机制或多或少是“隐藏的”。我们将任何内容放入 data 对象中,Vue 都会隐式地使其具有响应性。一方面,这简化了开发人员的工作,但另一方面,它也导致灵活性降低。

在幕后,Vue 2 使用 ES5 Object.defineProperty() 将 data 对象的所有属性转换为gettersetter。对于每个组件实例,Vue 创建一个依赖项观察器实例。在组件渲染期间作为依赖项收集/跟踪的任何属性都会由观察器记录。稍后,当依赖项的 setter 被触发时,观察器会收到通知,组件会重新渲染并更新视图。这基本上就是所有魔法的工作原理。不幸的是,有一些需要注意的地方。

更改检测注意事项

由于 Object.defineProperty() 的限制,Vue 无法检测某些数据更改。这些包括:

  • 向对象添加/删除属性(例如 obj.newKey = value)
  • 通过索引设置数组项(例如 arr[index] = newValue)
  • 修改数组的长度(例如 arr.length = newLength)

幸运的是,为了处理这些限制,Vue 为我们提供了 Vue.set API 方法,该方法向响应式对象添加一个属性,确保新属性也具有响应性,从而触发视图更新。

让我们在下面的示例中探索上述情况:

<code class="language-javascript"><div id="app">
  <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1>
  <button>Add "age" property</button>
  <p>Here are my favorite activities:</p>
  <ul>
    <li v-for="(item, index) in activities" :key="index">{{ item }} <button>Edit</button>
</li>
  </ul>
  <button>Clear the activities list</button>
</div></code>
<code class="language-javascript">const App = new Vue({
  el: '#app',
  data: {
    person: {
      name: "David"
    },
    activities: [
      "Reading books",
      "Listening music",
      "Watching TV"
    ]
  },
  methods: {
    // 1. Add a new property to an object
    addAgeProperty() {
      this.person.age = 30
    },
    // 2. Setting an array item by index
    editActivity(index) {
      const newValue = prompt('Input a new value')
      if (newValue) {
        this.activities[index] = newValue
      }
    },
    // 3. Modifying the length of the array
    clearActivities() {
      this.activities.length = 0
    }
  }
});</code>

在上面的示例中,我们可以看到这三种方法都不起作用。我们无法向 person 对象添加新属性。我们无法使用索引编辑 activities 数组中的项目。我们也无法修改 activities 数组的长度。

当然,这些情况有解决方法,我们将在下一个示例中探讨:

<code class="language-javascript">const App = new Vue({
  el: '#app',
  data: {
    person: {
      name: "David"
    },
    activities: [
      "Reading books",
      "Listening music",
      "Watching TV"
    ]
  },
  methods: {
    // 1. Adding a new property to the object
    addAgeProperty() {
      Vue.set(this.person, 'age', 30)
    },
    // 2. Setting an array item by index
    editActivity(index) {
      const newValue = prompt('Input a new value')
      if (newValue) {
        Vue.set(this.activities, index, newValue)
      }
    },
    // 3. Modifying the length of the array
    clearActivities() {
      this.activities.splice(0)
    }
  }
});</code>

在这个示例中,我们使用 Vue.set API 方法向 person 对象添加新的 age 属性,并从 activities 数组中选择/修改特定项目。在最后一种情况下,我们只使用 JavaScript 内置的 splice() 数组方法。

如我们所见,这有效,但这有点笨拙,并导致代码库不一致。幸运的是,在 Vue 3 中,这个问题已得到解决。让我们在下面的示例中看看它的神奇之处:

<code class="language-javascript">const App = {
  data() {
    return {
      person: {
        name: "David"
      },
      activities: [
        "Reading books",
        "Listening music",
        "Watching TV"
      ]
    }
  },
  methods: {
    // 1. Adding a new property to the object
    addAgeProperty() {
      this.person.age = 30
    },
    // 2. Setting an array item by index
    editActivity(index) {
      const newValue = prompt('Input a new value')
      if (newValue) {
        this.activities[index] = newValue
      }
    },
    // 3. Modifying the length of the array
    clearActivities() {
      this.activities.length = 0
    }
  }
}

Vue.createApp(App).mount('#app')</code>

在这个使用 Vue 3 的示例中,我们恢复到第一个示例中使用的内置 JavaScript 功能,现在所有方法都能正常工作。

在 Vue 2.6 中,引入了 Vue.observable() API 方法。它在某种程度上公开了响应式系统,允许开发人员显式地使对象具有响应性。实际上,这与 Vue 在内部包装 data 对象所使用的确切方法相同,对于为简单场景创建最小的跨组件状态存储很有用。但是,尽管它很有用,但这种单一方法无法与 Vue 3 附带的完整、功能丰富的响应式 API 的功能和灵活性相匹配。我们将在接下来的部分中看到原因。

注意:因为 Object.defineProperty() 只是一个 ES5 功能且无法模拟,所以 Vue 2 不支持 IE8 及以下版本。

Vue 3 响应式机制的工作原理

Vue 3 中的响应式系统已完全重写,以便利用 ES6 Proxy 和 Reflect API。新版本公开了一个功能丰富的响应式 API,使系统比以前更灵活、更强大。

Proxy API 允许开发人员拦截和修改目标对象上的低级对象操作。代理是对象的克隆/包装器(称为目标),并提供特殊函数(称为陷阱),这些函数响应特定操作并覆盖 JavaScript 对象的内置行为。如果您仍然需要使用默认行为,则可以使用相应的 Reflection API,其方法顾名思义,反映了 Proxy API 的方法。让我们探索一个示例,看看这些 API 如何在 Vue 3 中使用:

<code class="language-javascript"><div id="app">
  <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1>
  <button>Add "age" property</button>
  <p>Here are my favorite activities:</p>
  <ul>
    <li v-for="(item, index) in activities" :key="index">{{ item }} <button>Edit</button>
</li>
  </ul>
  <button>Clear the activities list</button>
</div></code>

要创建一个新的代理,我们使用 new Proxy(target, handler) 构造函数。它接受两个参数:目标对象(person 对象)和处理程序对象,该对象定义将拦截哪些操作(get 和 set 操作)。在处理程序对象中,我们使用 get 和 set 陷阱来跟踪何时读取属性以及何时修改/添加属性。我们设置控制台语句以确保方法正常工作。

get 和 set 陷阱采用以下参数:

  • target:由代理包装的目标对象
  • property:属性名称
  • value:属性值(此参数仅用于 set 操作)
  • receiver:在其上执行操作的对象(通常是代理)

Reflect API 方法接受与其对应的代理方法相同的参数。它们用于为给定操作实现默认行为,对于 get 陷阱是返回属性名称,对于 set 陷阱是如果设置了属性则返回 true,否则返回 false。

注释的 track() 和 trigger() 函数特定于 Vue,用于跟踪何时读取属性以及何时修改/添加属性。结果,Vue 会重新运行使用该属性的代码。

在示例的最后一部分,我们使用控制台语句输出原始 person 对象。然后,我们使用另一个语句读取代理对象的属性名称。接下来,我们修改 age 属性并创建一个新的 hobby 属性。最后,我们再次输出 person 对象以查看它是否已正确更新。

这就是 Vue 3 响应式机制的简要说明。当然,实际实现要复杂得多,但希望上面提供的示例足以让您掌握主要思想。

使用 Vue 3 响应式机制时,还需要考虑以下几点:

  • 它仅适用于支持 ES6 的浏览器
  • 响应式代理不等于原始对象

探索 Vue 3 响应式 API

最后,我们进入 Vue 3 响应式 API 本身。在以下部分,我们将探索按逻辑分组的 API 方法。我把方法分组是因为我认为以这种方式呈现更容易记住。让我们从基础开始。

基本方法

第一组包括用于控制数据响应性的最基本方法:

  • ref 接受基本值或普通对象,并返回一个响应式且可变的 ref 对象。ref 对象只有一个属性值,它指向基本值或普通对象。
  • reactive 接受一个对象并返回该对象的响应式副本。转换是深层次的,会影响所有嵌套属性。
  • readonly 接受 ref 或对象(普通或响应式),并返回原始对象的只读对象。转换是深层次的,会影响所有嵌套属性。
  • markRaw 返回对象本身,并阻止将其转换为代理对象。

现在让我们看看这些方法的实际应用:

<code class="language-javascript"><div id="app">
  <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1>
  <button>Add "age" property</button>
  <p>Here are my favorite activities:</p>
  <ul>
    <li v-for="(item, index) in activities" :key="index">{{ item }} <button>Edit</button>
</li>
  </ul>
  <button>Clear the activities list</button>
</div></code>

在这个示例中,我们探索了四种基本响应式方法的使用。

首先,我们创建一个值为 0 的 counter ref 对象。然后,在视图中,我们放置两个按钮,分别递增和递减 counter 的值。当我们使用这些按钮时,我们看到 counter 确实是响应式的。

其次,我们创建一个 person 响应式对象。然后,在视图中,我们放置两个输入控件,分别用于编辑人的姓名和年龄。当我们编辑人的属性时,它们会立即更新。

第三,我们创建一个 math 只读对象。然后,在视图中,我们设置一个按钮来使 math 的 PI 属性的值加倍。但是当我们单击按钮时,控制台中会显示一条错误消息,告诉我们该对象是只读的,我们无法修改其属性。

最后,我们创建一个 alphabetNumbers 对象,我们不想将其转换为代理,并将其标记为原始对象。它包含所有字母及其对应的数字(为简洁起见,此处仅使用前三个字母)。此顺序不太可能更改,因此我们故意将此对象保持为普通对象,这有利于性能。我们在表格中呈现对象内容,并设置一个按钮来将 B 属性的值更改为 3。我们这样做是为了表明,尽管对象可以被修改,但这不会导致视图重新渲染。

markRaw 非常适合我们不需要使其具有响应性的对象,例如很长的国家代码列表、颜色名称及其对应的十六进制数字等等。

最后,我们使用下一节中描述的类型检查方法来测试和确定我们创建的每个对象的类型。我们使用 onMounted() 生命周期钩子在应用程序最初呈现时触发这些检查。

类型检查方法

此组包含上面提到的所有四个类型检查器:

  • isRef 检查值是否为 ref 对象。
  • isReactive 检查对象是否是由 reactive 创建的响应式代理,或者是由 readonly 包装另一个由 reactive 创建的代理创建的。
  • isReadonly 检查对象是否是由 readonly 创建的只读代理。
  • isProxy 检查对象是否是由 reactive 或 readonly 创建的代理。

更多 Ref 方法

此组包含其他 ref 方法:

  • unref 返回 ref 的值。
  • triggerRef 手动执行与 shallowRef 绑定的任何效果。
  • customRef 创建一个自定义 ref,对它的依赖项跟踪和更新触发具有显式控制。

浅层方法

此组中的方法是 ref、reactive 和 readonly 的“浅层”等效项:

  • shallowRef 创建一个 ref,它只跟踪其 value 属性,而不使其值具有响应性。
  • shallowReactive 创建一个响应式代理,它只跟踪它自己的属性,不包括嵌套对象。
  • shallowReadonly 创建一个只读代理,它只使它自己的属性变为只读,不包括嵌套对象。

让我们通过检查以下示例来更容易地理解这些方法:

<code class="language-javascript"><div id="app">
  <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1>
  <button>Add "age" property</button>
  <p>Here are my favorite activities:</p>
  <ul>
    <li v-for="(item, index) in activities" :key="index">{{ item }} <button>Edit</button>
</li>
  </ul>
  <button>Clear the activities list</button>
</div></code>

这个示例从创建一个 settings shallow ref 对象开始。然后,在视图中,我们添加两个输入控件来编辑其 width 和 height 属性。但是当我们尝试修改它们时,我们看到它们没有更新。为了解决这个问题,我们添加一个按钮,它会更改整个对象及其所有属性。现在它可以工作了。这是因为 value 的内容(width 和 height 作为单个属性)没有转换为响应式对象,但 value 的变异(整个对象)仍然被跟踪。

接下来,我们创建一个 settingsA shallow reactive 代理,它包含 width 和 height 属性以及一个嵌套的 coords 对象,其中包含 x 和 y 属性。然后,在视图中,我们为每个属性设置一个输入控件。当我们修改 width 和 height 属性时,我们看到它们会响应式地更新。但是当我们尝试修改 x 和 y 属性时,我们看到它们没有被跟踪。

最后,我们创建一个 settingsB shallow readonly 对象,它与 settingsA 具有相同的属性。在这里,当我们尝试修改 width 或 height 属性时,控制台中会显示一条错误消息,告诉我们该对象是只读的,我们无法修改其属性。另一方面,可以毫无问题地修改 x 和 y 属性。

来自最后两个示例的嵌套 coords 对象不受转换的影响,并且保持为普通对象。这意味着它可以自由修改,但它的任何修改都不会被 Vue 跟踪。

转换方法

接下来的三种方法用于将代理转换为 ref(s) 或普通对象:

  • toRef 为源响应式对象上的属性创建一个 ref。ref 保持与其源属性的响应式连接。
  • toRefs 将响应式对象转换为普通对象。普通对象的每个属性都是一个指向原始对象相应属性的 ref。
  • toRaw 返回响应式或只读代理的原始普通对象。

让我们看看这些转换如何在下面的示例中工作:

<code class="language-javascript">const App = new Vue({
  el: '#app',
  data: {
    person: {
      name: "David"
    },
    activities: [
      "Reading books",
      "Listening music",
      "Watching TV"
    ]
  },
  methods: {
    // 1. Add a new property to an object
    addAgeProperty() {
      this.person.age = 30
    },
    // 2. Setting an array item by index
    editActivity(index) {
      const newValue = prompt('Input a new value')
      if (newValue) {
        this.activities[index] = newValue
      }
    },
    // 3. Modifying the length of the array
    clearActivities() {
      this.activities.length = 0
    }
  }
});</code>

在这个示例中,我们首先创建一个基本 person 响应式对象,我们将将其用作源对象。

然后,我们将 person 的 name 属性转换为具有相同名称的 ref。然后,在视图中,我们添加两个输入控件——一个用于 name ref,另一个用于 person 的 name 属性。当我们修改其中一个时,另一个也会相应地更新,因此它们之间的响应式连接得以保持。

接下来,我们将 person 的所有属性转换为包含在 personDetails 对象中的单个 ref。然后,在视图中,我们再次添加两个输入控件来测试我们刚刚创建的一个 ref。如我们所见,personDetails 的 age 与 person 的 age 属性完全同步,就像在前面的示例中一样。

最后,我们将 person 响应式对象转换为 rawPerson 普通对象。然后,在视图中,我们添加一个输入控件来编辑 rawPerson 的 hobby 属性。但是如我们所见,Vue 不会跟踪转换后的对象。

computed 和 watch 方法

最后一组方法用于计算复杂值和“监视”特定值:

  • computed 接受 getter 函数作为参数,并返回一个不可变的响应式 ref 对象。
  • watchEffect 立即运行一个函数,并响应式地跟踪其依赖项,并在依赖项更改时重新运行它。
  • watch 与 Options API 的 this.$watch 和相应的 watch 选项完全等效。它正在监视特定的数据源,并在监视的源发生更改时在回调函数中应用副作用。

让我们考虑以下示例:

<code class="language-javascript"><div id="app">
  <h1>Hello! My name is {{ person.name }}. I'm {{ person.age }} years old.</h1>
  <button>Add "age" property</button>
  <p>Here are my favorite activities:</p>
  <ul>
    <li v-for="(item, index) in activities" :key="index">{{ item }} <button>Edit</button>
</li>
  </ul>
  <button>Clear the activities list</button>
</div></code>

在这个示例中,我们创建一个 fullName computed 变量,它基于 firstName 和 lastName ref 进行计算。然后,在视图中,我们添加两个输入控件来编辑全名的两个部分。如我们所见,当我们修改任何一个部分时,fullName 会重新计算,结果也会更新。

接下来,我们创建一个 volume ref 并为其设置一个 watch effect。每次修改 volume 时,effect 都会运行回调函数。为了证明这一点,在视图中,我们添加一个按钮,将 volume 递增 1。我们在回调函数中设置一个条件,测试 volume 的值是否可以被 3 整除,当它返回 true 时,会显示一个警报消息。effect 在应用程序启动时运行一次,并设置 volume 的值,然后在每次修改 volume 的值时再次运行。

最后,我们创建一个 state ref 并设置一个 watch 函数来跟踪它的更改。一旦 state 发生更改,回调函数就会执行。在这个示例中,我们添加一个按钮,在 playing 和 paused 之间切换 state。每次发生这种情况时,都会显示一条警报消息。

watchEffect 和 watch 在功能方面看起来非常相似,但它们有一些明显的区别:

  • watchEffect 将回调函数中包含的所有响应式属性都视为依赖项。因此,如果回调包含三个属性,则所有这些属性都会被隐式地跟踪更改。
  • watch 只跟踪我们在回调中作为参数包含的属性。此外,它还提供监视属性的先前值和当前值。

如您所见,Vue 3 响应式 API 提供了大量方法,可用于各种用例。API 非常庞大,在本教程中,我只探讨了基础知识。有关更深入的探索、细节和边缘情况,请访问响应式 API 文档。

结论

在本文中,我们介绍了什么是响应式系统以及它如何在 Vue 2 和 Vue 3 中实现。我们看到 Vue 2 有一些缺点在 Vue 3 中得到了成功解决。Vue 3 响应式机制是基于现代 JavaScript 功能的完整重写。让我们总结一下它的优缺点。

优点:

  • 它可以用作独立包。例如,您可以将其与 React 一起使用。
  • 由于其功能丰富的 API,它提供了更大的灵活性和功能。
  • 它支持更多的数据结构(Map、WeakMap、Set、WeakSet)。
  • 它的性能更好。只有所需的数据才会变为响应式。
  • Vue 2 中的数据操作注意事项已得到解决。

缺点:

  • 它仅适用于支持 ES6 的浏览器。
  • 就身份比较 (===) 而言,响应式代理不等于原始对象。
  • 与 Vue 2 的“自动”响应式机制相比,它需要更多代码。

底线是 Vue 3 响应式机制是一个灵活且强大的系统,Vue 和非 Vue 开发人员都可以使用它。无论您的情况如何,只需抓住它并开始构建令人惊叹的东西即可。

Vue 3 响应式系统常见问题 (FAQ)

什么是 Vue 3 响应式系统?

Vue 3 响应式系统是 Vue.js(一个流行的 JavaScript 框架)的一个基本方面。它负责跟踪应用程序状态中的更改并更新 DOM(文档对象模型)以反映这些更改。该系统围绕“响应式对象”的概念构建。当这些对象的属性发生更改时,Vue 会自动触发必要的更新。这使开发人员更容易构建动态、响应式的 Web 应用程序。

Vue 3 响应式系统与 Vue 2 有何不同?

Vue 3 的响应式系统已使用 JavaScript 的 Proxy 对象完全重写,与 Vue 2 的 Object.defineProperty 方法相比,它提供了一种更高效、更强大的跟踪更改的方式。这个新系统允许 Vue 跟踪嵌套属性的更改,这是 Vue 2 的一个限制。它还减少了内存占用并提高了性能。

如何在我的应用程序中使用 Vue 3 响应式系统?

要使用 Vue 3 响应式系统,您需要使用 reactive() 函数包装您的数据。此函数使对象及其属性具有响应性。当属性更改时,Vue 将自动重新渲染依赖于它的组件。您还可以使用 ref() 函数使单个变量具有响应性。

ref 函数在 Vue 3 响应式机制中的作用是什么?

Vue 3 中的 ref() 函数用于创建对值的响应式引用。它将值包装在一个具有单个属性“.value”的对象中,并使此对象具有响应性。这意味着当值更改时,使用此 ref 的任何组件都将更新。

Vue 3 中 reactive 和 ref 有什么区别?

reactive() 和 ref() 都用于在 Vue 3 中使数据具有响应性,但它们用于不同的场景。reactive() 函数用于使对象具有响应性,而 ref() 函数用于使基本值(如字符串或数字)具有响应性。但是,ref() 也可以与对象一起使用,在这种情况下,它的行为类似于 reactive()。

Vue 3 如何处理数组的响应式?

Vue 3 处理数组的响应式方式与处理对象的方式相同。如果您使用 reactive() 函数使数组具有响应性,Vue 将跟踪对数组元素及其长度的更改。这意味着如果您添加、删除或替换元素,Vue 将更新依赖于该数组的组件。

Vue 3 响应式机制中的 toRefs 函数是什么?

Vue 3 中的 toRefs() 函数用于将响应式对象转换为普通对象,其中原始对象的每个属性都表示为一个 ref。当您想要解构响应式对象但仍保持其响应性时,这很有用。

如何阻止 Vue 3 中的对象具有响应性?

您可以使用 markRaw() 函数来防止对象具有响应性。这在某些情况下很有用,在这些情况下,您不希望 Vue 跟踪对对象的更改。

Vue 3 响应式机制中的 computed 函数是什么?

Vue 3 中的 computed() 函数用于创建一个依赖于其他响应式属性的响应式属性。当任何依赖项发生更改时,computed 属性的值会自动更新。这对于计算或转换非常有用,这些计算或转换应在底层数据更改时更新。

Vue 3 如何处理 Map 和 Set 的响应式?

Vue 3 的响应式系统完全支持 JavaScript 的 Map 和 Set 数据结构。如果您使 Map 或 Set 具有响应性,Vue 将分别跟踪其条目或元素的更改。这意味着如果您添加、删除或替换条目或元素,Vue 将更新依赖于 Map 或 Set 的组件。

以上是了解VUE 3中的新反应性系统的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn