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 响应式原理的资料请关注脚本之家其它相关文章!
