Vue 3 中的响应式原理可谓是非常之重要,通过学习 Vue3 的响应式原理,不仅能让我们学习到 Vue.js 的一些设计模式和思想,还能帮助我们提高项目开发效率和代码调试能力。
一、Vue 3 响应式使用
1. Vue 3 中的使用
当我们在学习 Vue 3 的时候,可以通过一个简单示例,看看什么是 Vue 3 中的响应式:
<!-- HTML 内容 -->
<div id="app">
<div>Price: {{price}}</div>
<div>Total: {{price * quantity}}</div>
<div>getTotal: {{getTotal}}</div>
</div>
const app = Vue.createApp({ // ① 创建 APP 实例
data() {
return {
price: 10,
quantity: 2
}
},
computed: {
getTotal() {
return this.price * this.quantity * 1.1
}
}
})
app.mount('#app') // ② 挂载 APP 实例
通过创建 APP 实例和挂载 APP 实例即可,这时可以看到页面中分别显示对应数值:
当我们修改 price
或 quantity
值的时候,页面上引用它们的地方,内容也能正常展示变化后的结果。这时,我们会好奇为何数据发生变化后,相关的数据也会跟着变化,那么我们接着往下看。
2. 实现单个值的响应式
在普通 JS 代码执行中,并不会有响应式变化,比如在控制台执行下面代码:
let price = 10, quantity = 2;
const total = price * quantity;
console.log(`total: ${total}`); // total: 20
price = 20;
console.log(`total: ${total}`); // total: 20
从这可以看出,在修改 price
变量的值后, total
的值并没有发生改变。
那么如何修改上面代码,让 total
能够自动更新呢?我们其实可以将修改 total
值的方法保存起来,等到与 total
值相关的变量(如 price
或 quantity
变量的值)发生变化时,触发该方法,更新 total
即可。我们可以这么实现:
let price = 10, quantity = 2, total = 0;
const dep = new Set(); // ①
const effect = () => { total = price * quantity };
const track = () => { dep.add(effect) }; // ②
const trigger = () => { dep.forEach( effect => effect() )}; // ③
track();
console.log(`total: ${total}`); // total: 0
trigger();
console.log(`total: ${total}`); // total: 20
price = 20;
trigger();
console.log(`total: ${total}`); // total: 40
上面代码通过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 Set
类型的 dep
变量,用来存放需要执行的副作用( effect
函数),这边是修改 total
值的方法;
② 创建 track()
函数,用来将需要执行的副作用保存到 dep
变量中(也称收集副作用);
③ 创建 trigger()
函数,用来执行 dep
变量中的所有副作用;
在每次修改 price
或 quantity
后,调用 trigger()
函数执行所有副作用后, total
值将自动更新为最新值。
3. 实现单个对象的响应式
通常,我们的对象具有多个属性,并且每个属性都需要自己的 dep
。我们如何存储这些?比如:
let product = { price: 10, quantity: 2 };
从前面介绍我们知道,我们将所有副作用保存在一个 Set
集合中,而该集合不会有重复项,这里我们引入一个 Map
类型集合(即 depsMap
),其 key
为对象的属性(如: price
属性), value
为前面保存副作用的 Set
集合(如: dep
对象),大致结构如下图:
实现代码:
let product = { price: 10, quantity: 2 }, total = 0;
const depsMap = new Map(); // ①
const effect = () => { total = product.price * product.quantity };
const track = key => { // ②
let dep = depsMap.get(key);
if(!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
const trigger = key => { // ③
let dep = depsMap.get(key);
if(dep) {
dep.forEach( effect => effect() );
}
};
track('price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger('price');
console.log(`total: ${total}`); // total: 40
上面代码通过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 Map
类型的 depsMap
变量,用来保存每个需要响应式变化的对象属性(key
为对象的属性, value
为前面 Set
集合);
② 创建 track()
函数,用来将需要执行的副作用保存到 depsMap
变量中对应的对象属性下(也称收集副作用);
③ 创建 trigger()
函数,用来执行 dep
变量中指定对象属性的所有副作用;
这样就实现监听对象的响应式变化,在 product
对象中的属性值发生变化, total
值也会跟着更新。
4. 实现多个对象的响应式
如果我们有多个响应式数据,比如同时需要观察对象 a
和对象 b
的数据,那么又要如何跟踪每个响应变化的对象?
这里我们引入一个 WeakMap 类型的对象,将需要观察的对象作为 key
,值为前面用来保存对象属性的 Map 变量。代码如下:
let product = { price: 10, quantity: 2 }, total = 0;
const targetMap = new WeakMap(); // ① 初始化 targetMap,保存观察对象
const effect = () => { total = product.price * product.quantity };
const track = (target, key) => { // ② 收集依赖
let depsMap = targetMap.get(target);
if(!depsMap){
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if(!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
const trigger = (target, key) => { // ③ 执行指定对象的指定属性的所有副作用
const depsMap = targetMap.get(target);
if(!depsMap) return;
let dep = depsMap.get(key);
if(dep) {
dep.forEach( effect => effect() );
}
};
track(product, 'price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger(product, 'price');
console.log(`total: ${total}`); // total: 40
上面代码通过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 WeakMap
类型的 targetMap
变量,用来要观察每个响应式对象;
② 创建 track()
函数,用来将需要执行的副作用保存到指定对象( target
)的依赖中(也称收集副作用);
③ 创建 trigger()
函数,用来执行指定对象( target
)中指定属性( key
)的所有副作用;
这样就实现监听对象的响应式变化,在 product
对象中的属性值发生变化, total
值也会跟着更新。
大致流程如下图: