一、Vue 3 响应式使用 1 实现单个值的响应式 在普通 JS 代码执行中,并不会有响应式变化,比如在控制台执行下面代码:
1 2 3 4 5 let price = 10 , quantity = 2 ;const total = price * quantity;console .log(`total: ${total} ` ); price = 20 ; console .log(`total: ${total} ` );
从这可以看出,在修改 price
变量的值后, total
的值并没有发生改变。
那么如何修改上面代码,让 total
能够自动更新呢?我们其实可以将修改 total
值的方法保存起来,等到与 total
值相关的变量(如 price
或 quantity
变量的值)发生变化时,触发该方法,更新 total
即可。我们可以这么实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 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} ` ); trigger(); console .log(`total: ${total} ` ); price = 20 ; trigger(); console .log(`total: ${total} ` );
上面代码通过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 Set
类型的 dep
变量,用来存放需要执行的副作用( effect
函数),这边是修改 total
值的方法;
② 创建 track()
函数,用来将需要执行的副作用保存到 dep
变量中(也称收集副作用);
③ 创建 trigger()
函数,用来执行 dep
变量中的所有副作用;
在每次修改 price
或 quantity
后,调用 trigger()
函数执行所有副作用后, total
值将自动更新为最新值。
2 实现单个对象的响应式 通常,我们的对象具有多个属性,并且每个属性都需要自己的 dep
。我们如何存储这些?比如:
1 let product = { price : 10 , quantity : 2 };
从前面介绍我们知道,我们将所有副作用保存在一个 Set
集合中,而该集合不会有重复项,这里我们引入一个 Map
类型集合(即 depsMap
),其 key
为对象的属性(如: price
属性), value
为前面保存副作用的 Set
集合(如: dep
对象),大致结构如下图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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} ` ); effect(); console .log(`total: ${total} ` ); product.price = 20 ; trigger('price' ); console .log(`total: ${total} ` );
上面代码通过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 Map
类型的 depsMap
变量,用来保存每个需要响应式变化的对象属性(key
为对象的属性, value
为前面 Set
集合);
② 创建 track()
函数,用来将需要执行的副作用保存到 depsMap
变量中对应的对象属性下(也称收集副作用);
③ 创建 trigger()
函数,用来执行 dep
变量中指定对象属性的所有副作用;
这样就实现监听对象的响应式变化,在 product
对象中的属性值发生变化, total
值也会跟着更新。
3 实现多个对象的响应式 如果我们有多个响应式数据,比如同时需要观察对象 a
和对象 b
的数据,那么又要如何跟踪每个响应变化的对象?
这里我们引入一个 WeakMap 类型 的对象,将需要观察的对象作为 key
,值为前面用来保存对象属性的 Map 变量。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 let product = { price : 10 , quantity : 2 }, total = 0 ;const targetMap = new WeakMap (); 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} ` ); effect(); console .log(`total: ${total} ` ); product.price = 20 ; trigger(product, 'price' ); console .log(`total: ${total} ` );
上面代码通过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 WeakMap
类型的 targetMap
变量,用来要观察每个响应式对象;
② 创建 track()
函数,用来将需要执行的副作用保存到指定对象( target
)的依赖中(也称收集副作用);
③ 创建 trigger()
函数,用来执行指定对象( target
)中指定属性( key
)的所有副作用;
这样就实现监听对象的响应式变化,在 product
对象中的属性值发生变化, total
值也会跟着更新。
大致流程如下图:
二、Proxy 和 Reflect 在上一节内容中,介绍了如何在数据发生变化后,自动更新数据,但存在的问题是,每次需要手动通过触发 track()
函数搜集依赖,通过 trigger()
函数执行所有副作用,达到数据更新目的。
这一节将来解决这个问题,实现这两个函数自动调用。
1. 如何实现自动操作 这里我们引入 JS 对象访问器的概念,解决办法如下:
在读取(GET 操作)数据时,自动执行 track()
函数自动收集依赖;
在修改(SET 操作)数据时,自动执行 trigger()
函数执行所有副作用;
那么如何拦截 GET 和 SET 操作?接下来看看 Vue2 和 Vue3 是如何实现的:
需要注意的是: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 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法如下
1 const p = new Proxy(target, handler)
参数如下:
target : 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler : 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p
的行为
1 2 3 4 5 6 7 8 9 10 let product = { price : 10 , quantity : 2 };let proxiedProduct = new Proxy (product, { get (target, key ) { console .log('正在读取的数据:' ,key); return target[key]; } }) console .log(proxiedProduct.price);
然后结合 Reflect 使用,只需修改 get 函数:
1 2 3 4 get (target, key, receiver ) { console .log('正在读取的数据:' ,key); return Reflect .get(target, key, receiver); }
输出结果还是一样。
接下来增加 set 函数,来拦截对象的修改操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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);
4. 修改 track 和 trigger 函数 通过上面代码,我们已经实现一个简单 reactive()
函数,用来将普通对象转换为响应式对象 。但是还缺少自动执行 track()
函数和 trigger()
函数,接下来修改上面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const targetMap = new WeakMap ();let total = 0 ;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() ); } }; const reactive = (target ) => { const handler = { get (target, key, receiver ) { console .log('正在读取的数据:' ,key); const result = Reflect .get(target, key, receiver); track(target, key); return result; }, set (target, key, value, receiver ) { console .log('正在修改的数据:' , key, ',值为:' , value); const oldValue = target[key]; const result = Reflect .set(target, key, value, receiver); if (oldValue != result){ trigger(target, key); } return result; } } return new Proxy (target, handler); } let product = reactive({price : 10 , quantity : 2 }); effect(); console .log(total); product.price = 20 ; console .log(total);
三、activeEffect 和 ref 在上一节代码中,还存在一个问题: track
函数中的依赖( effect
函数)是外部定义的,当依赖发生变化, track
函数收集依赖时都要手动修改其依赖的方法名。
比如现在的依赖为 foo
函数,就要修改 track
函数的逻辑,可能是这样:
1 2 3 4 5 const foo = () => { };const track = (target, key ) => { dep.add(foo); }
1. 引入 activeEffect 变量 接下来引入 activeEffect
变量,来保存当前运行的 effect 函数。
1 2 3 4 5 6 let activeEffect = null ;const effect = eff => { activeEffect = eff; activeEffect(); activeEffect = null ; }
然后在 track
函数中将 activeEffect
变量作为依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 const track = (target, key ) => { if (activeEffect) { 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(activeEffect); } }
1 2 3 effect(() => { total = product.price * product.quantity });
这样就可以解决手动修改依赖的问题,这也是 Vue3 解决该问题的方法
2. 引入 ref 方法 熟悉 Vue3 Composition API 的朋友可能会想到 Ref,它接收一个值,并返回一个响应式可变的 Ref 对象 ,其值可以通过 value
属性获取。
ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。
官网的使用示例如下:
1 2 3 4 5 const count = ref(0 )console .log(count.value) count.value++ console .log(count.value)
我们有 2 种方法实现 ref 函数:
使用 rective
函数 1 const ref = intialValue => reactive({value : intialValue});
使用对象的属性访问器(计算属性) 属性方式去包括:getter 和 setter 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const ref = raw => { const r = { get value (){ track(r, 'value' ); return raw; }, set value (newVal ){ raw = newVal; trigger(r, 'value' ); } } return r; }
四、完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 const targetMap=new WeakMap ()let activeEffect=null function effect (eff ) { activeEffect=eff activeEffect() activeEffect=null } function track (target,key ) { if (activeEffect){ 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(activeEffect) } } function trigger (target,key ) { const depsMap=targetMap.get(target) if (!depsMap)return const dep=depsMap.get(key) if (dep){ dep.forEach(effect => { effect() }) } } function reactive (target ) { const handler={ get (target,key,receiver ) { let res=Reflect .get(target,key,receiver) track(target,key) return res }, set (target,key,value,receiver ) { let oldValue=target[key] let res=Reflect .set(target,key,value,receiver) if (res&&oldValue!=value) { trigger(target,key) } return res } } return new Proxy (target,handler) } function ref (raw ) { const r={ get value (){ track(r,'value' ) return raw }, set value (newval ){ raw=newval trigger(r,'value' ) } } return r; } let product=reactive({prie :5 ,quantity :2 })let salePrice=ref(0 )let total=0 effect(()=> { salePrice.value=product.price*0.9 }) effect(()=> { total=salePrice.value*product.quantity }) console .log(`Before updated quantity total=${total} salePrice=${salePrice.value} ` )product.quantity=3 console .log(`After updated quantity total=${total} salePrice=${salePrice.value} ` )product.price=10 console .log(`After updated quantity total=${total} salePrice=${salePrice.value} ` )