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 代码执行中,并不会有响应式变化,比如在控制台执行下面代码:
const app = Vue.createApp({ // ① 创建 APP 实例 data() { return { price: 10, quantity: 2 } }, computed: { getTotal() { return this.price * this.quantity * 1.1 } } }) app.mount('#app') // ② 挂载 APP 实例
从这可以看出,在修改 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 值将自动更新为最新值。
(图片来源:Vue Mastery)
3. 实现单个对象的响应式
通常,我们的对象具有多个属性,并且每个属性都需要自己的 dep
。我们如何存储这些?比如:
let product = { price: 10, quantity: 2 };
(图片来源:Vue Mastery)
实现代码:
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
值也会跟着更新。
大致流程如下图:
(图片来源:Vue Mastery)
二、Proxy 和 Reflect
在上一节内容中,介绍了如何在数据发生变化后,自动更新数据,但存在的问题是,每次需要手动通过触发 track()
函数搜集依赖,通过 trigger()
函数执行所有副作用,达到数据更新目的。
这一节将来解决这个问题,实现这两个函数自动调用。
1. 如何实现自动操作
这里我们引入 JS 对象访问器的概念,解决办法如下:
- 在读取(GET 操作)数据时,自动执行
track()
函数自动收集依赖; - 在修改(SET 操作)数据时,自动执行
trigger()
函数执行所有副作用;
那么如何拦截 GET 和 SET 操作?接下来看看 Vue2 和 Vue3 是如何实现的:
- 在 Vue2 中,使用 ES5 的 Object.defineProperty() 函数实现;
- 在 Vue3 中,使用 ES6 的 Proxy 和 Reflect API 实现;
需要注意的是:Vue3 使用的 Proxy
和 Reflect
API 并不支持 IE。
Object.defineProperty() 函数这边就不多做介绍,可以阅读文档,下文将主要介绍 Proxy 和 Reflect API。
2. 如何使用 Reflect
通常我们有三种方法读取一个对象的属性:
- 使用
.
操作符:leo.name
; - 使用
[]
:leo['name']
; - 使用
Reflect
API:Reflect.get(leo, 'name')
。
这三种方式输出结果相同。
3. 如何使用 Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法如下:
const p = new Proxy(target, handler)
参数如下:
- target : 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
- handler : 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理
p
的行为。
我们通过官方文档,体验一下 Proxy API:
let product = { price: 10, quantity: 2 }; let proxiedProduct = new Proxy(product, { get(target, key){ console.log('正在读取的数据:',key); return target[key]; } }) console.log(proxiedProduct.price); // 正在读取的数据: price // 10
这样就保证我们每次在读取 proxiedProduct.price
都会执行到其中代理的 get 处理函数。其过程如下:
(图片来源:Vue Mastery)
然后结合 Reflect 使用,只需修改 get 函数:
get(target, key, receiver){ console.log('正在读取的数据:',key); return Reflect.get(target, key, receiver); }
输出结果还是一样。
接下来增加 set 函数,来拦截对象的修改操作:
let product = { price: 10, quantity: 2 }; let proxiedProduct = new Proxy(product, { get(target, key, receiver){ console.log('正在读取的数据:',key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver){ console.log('正在修改的数据:', key, ',值为:', value); return Reflect.set(target, key, value, receiver); } }) proxiedProduct.price = 20; console.log(proxiedProduct.price); // 正在修改的数据: price ,值为: 20 // 正在读取的数据: price // 20
这样便完成 get 和 set 函数来拦截对象的读取和修改的操作。为了方便对比 Vue 3 源码,我们将上面代码抽象一层,使它看起来更像 Vue3 源码:
function reactive(target){ const handler = { // ① 封装统一处理函数对象 get(target, key, receiver){ console.log('正在读取的数据:',key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver){ console.log('正在修改的数据:', key, ',值为:', value); return Reflect.set(target, key, value, receiver); } } return new Proxy(target, handler); // ② 统一调用 Proxy API } let product = reactive({price: 10, quantity: 2}); // ③ 将对象转换为响应式对象 product.price = 20; console.log(product.price); // 正在修改的数据: price ,值为: 20 // 正在读取的数据: price // 20