Vue3和Vue2的响应式原理
作者:书源
什么是响应式?
简单来说,响应式就是页面的模版内容能随着数据变化而重新渲染。我们来看一个案例:
<template> <div id="app"> <div>Count: {{ count }}</div> <button class="btn" @click="onAdd">Add</button> </div> </template> <script setup> import { ref } from "vue"; const count = ref(1); const onAdd = () => { count.value++; }; </script>
在这段代码中,ref
是 Vue3 官方提供的响应式 API,count
就是通过响应式 API 声明的响应式数据。count
这个数据可以直接在模板里使用,例如上述代码中我们使用了count
数据进行显示。
那么响应式体现在哪呢?上述代码有一个点击事件的函数 onAdd
,这个函数实现了对 count
数值的自增操作。每当点击这个 onAdd
事件,count
就会自动加 1,对应使用到 count
的模板也会随之重新渲染最新的数据。
上述功能代码里的这种视图随着数据的变化,就是响应式的特征。基于 Vue的响应式 API 生成的数据,就是响应式数据,如果响应式数据发生了变化,那么依赖了数据的视图也会跟着变化。
那响应式原理是如何实现的呢?接下来我们就一起来看下。
响应式原理
我们以一个经常被拿来当作典型例子的用例即是 Excel 表格:
这里单元格 A2 中的值是通过公式 = 5 * A1
来定义的 ,我们期望更改 A1, A2 也随即自动更新。
而 JavaScript 默认并不是这样的。如果我们用 JavaScript 写类似的逻辑:
let A1 = 1; let A2 = 5 * A1; console.log(A2); // 5A1 = 2;console.log(A2); // 仍然是 5
我们现在的目标是,在 A1
变化时会调用 effect()
(产生作用)。在具体实现时,Vue2和Vue3采取了不同的实现方案,我们一一看下【备注:更完善的响应式原理会在专栏后续补充】。
Vue2响应式方案
Vue2的实现方案主要是借助于Object.defineProperty
。
这里来看下如下通过Object.defineProperty
,来实现我们的目标:
let obj = {}; let A1; let A2; function effect() { A2 = 5 * A1; } Object.defineProperty(obj, "A1", { get() { return A1; }, set(val) { A1 = val; effect(); }, }); obj.A1 = 1; console.log(A2); // 打印5obj.A1 = 2;console.log(A2); // 打印10
在上面的实现中,我们定义个一个对象 obj,使用 Object.defineProperty 代理 A1 属性,进而对 obj 对象的 value 属性实现了拦截,读取 A1 属性的时候执行 get 函数,修改 A1 属性的时候执行 set 函数,在 set 函数内部调用 effect() 函数,从而实现响应式。
接下来我们来看下Vue3的响应式方案,不过它分非原始值的响应式和原始值的响应式。
非原始值的响应式方案
什么是非原始值呢?其实就是对象,而并非数字、字符串。
非原始值的响应式数据是基于Proxy
实现的,它允许我们拦截并重新定义一个对象的基本操作,具体API 我们可以访问MDN 文档。
这里来看下如下通过Proxy
,来实现我们的目标:
let obj = {}; let A1; let A2; function effect(val) { A2 = 5 * val; } let proxy = new Proxy(obj, { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; effect(value); }, }); proxy.A1 = 1; console.log(A2); // 打印5proxy.A1 = 2;console.log(A2); // 打印10
在上面的实现中,我们定义了一个对象 obj,使用 Proxy 代理 obj,实现了和Vue2相同的功能 ,读取 A1 属性的时候执行 get 函数,修改 A1 属性的时候执行 set 函数,在 set 函数内部调用 effect() 函数,从而实现响应式。
与Object.defineProperty所不同的是Proxy 是针对对象来监听,而不是针对某个具体属性,所以不仅可以代理那些定义时不存在的属性,还可以代理更丰富的数据结构,比如 Map、Set 等等。
原始值的响应式方案
什么是原始值?原始值指的是Boolean、Number、String、null等类型的值。
在JavaScript中,原始值是按值传递的,而非按引用传递。如果一个函数接收原始值作为参数,那么形参和实参之间没有引用关系,是两个完全独立的值。此外,JavaScript中的proxy无法提供对原始值的代理,想要将原始值变成响应式数据,就必须对其做一层包裹,借助对象的 get 和 set 函数来实现:
let A1 = 1; let A2; function effect(val) { A2 = 5 * val; } let obj = { get value() { return A1; }, set value(val) { A1 = val; effect(val); }, }; obj.value = 1; console.log(A2); // 打印5obj.value = 2;console.log(A2); // 打印10
在上面的实现中,我们利用对象的 get 和 set 函数来进行监听,这种响应式的实现方式,只能拦截某一个属性,这也是 Vue 3 中 ref 这个 API 的实现。
以上我们了解了响应式原理,不过我们想在工作中更好地运用响应式API,我们还需要知道在响应式开发中可能会遇到什么“坑”。
响应式原理注意事项
我们先来了解一下 Vue 2的注意事项,由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
Vue 2注意事项
对于对象,Vue2 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data
对象上存在才能让 Vue 将它转换为响应式的。例如:
var vm = new Vue({ data: { a: 1 } }); // `vm.a` 是响应式的vm.b = 2// `vm.b` 是非响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式 property。例如:
Vue.set(vm.someObject, "b", 2);
您还可以使用 vm.$set
实例方法,这也是全局 Vue.set
方法的别名:
this.$set(this.someObject, "b", 2);
对于数组,Vue2 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[index] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
举个例子:
var vm = new Vue({ data: { items: ["a", "b", "c"] } }); vm.items[1] = "x"; // 不是响应性的vm.items.length = 2 // 不是响应性的
为了解决第一类问题,以下方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也将在响应式系统内触发状态更新:
// Vue.setVue.set(vm.items, indexOfItem, newValue) // Array.prototype.splicevm.items.splice(indexOfItem, 1, newValue) // 该方法是全局方法 Vue.set 的一个别名 vm.$set(vm.items, indexOfItem, newValue);
为了解决第二类问题,你可以使用 splice
:
vm.items.splice(newLength);
Vue 3注意事项
Vue.js 3 的响应式开发有什么需要注意的?
第一个注意事项是响应式数据解构,这可能会丢失响应式联系,例如:
<template> <div id="app"> <input v-model="text" placeholder="文本信息" /> 文本信息:{{ text }} </div> </template> <script setup> import { reactive } from "vue"; const state = reactive({ text: "hello world" }); const { text } = state; </script>
在上述代码中,text 的响应式联系并不会生效,修改 text 内容后,不会触发页面的展示文本 text 的视图更新渲染。这是为什么呢?
这里我们用 reactive 定义了 state 响应式数据,但是在之后又把其中的 state.text 解构赋值给了变量 text,这就会断掉响应式联系,导致再怎么更新 text 都不会触发视图重新更新渲染。
那如果我们就想用解构的方法来使用text变量怎么办?可以借助toRefs
来实现:
<script setup> import { reactive, toRefs } from "vue"; const state = reactive({ text: "hello world" }); const { text } = toRefs(state); </script>
第二个注意事项是Vue3官方指明谨慎使用的API,例如 shallowReactive 、 shallowReadonly等等,示例:
const state = shallowReactive({ foo: 1, nested: { bar: 2 } }) // 更改状态自身的属性是响应式的 state.foo++ // ...但下层嵌套对象不会被转为响应式 isReactive(state.nested) // false // 不是响应式的 state.nested.bar++
异步更新队列
异步队列更新,这是Vue2和Vue3都共存的注意事项,当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用数据驱动的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。
nextTick()
可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。示例:
<script setup> import { ref, nextTick } from 'vue' const count = ref(0) async function increment() { count.value++ // DOM 还未更新 console.log(document.getElementById('counter').textContent) // 0 await nextTick() // DOM 此时已经更新 console.log(document.getElementById('counter').textContent) // 1 } </script> <template> <button id="counter" @click="increment">{{ count }}</button> </template>
总结
今天的知识点我们来做个总结:
1、我们认识了什么是响应式,其实就是页面的模版内容能随着数据变化而重新渲染。
2、我们总结了Vue2和Vue3各自的响应式实现原理以及开发需要注意的事项。
以上就是Vue3和Vue2的响应式原理的详细内容,更多关于Vue3和Vue2 响应式原理的资料请关注脚本之家其它相关文章!