浅谈 vue3 响应式原理 第二篇 记录与重放
上一篇简单讲了一下什么是响应式,实现响应式的基本套路:在变量被设置新值的时候,重新执行一次副作用。
但是副作用有很多,我们不可能把每个副作用的执行都在 Proxy
中硬编码一遍,如何设计才能管理这么多副作用,这次来讲 vue3 实现响应式需要的数据结构。
副作用的存储
要完成响应式,我们上一篇讲了在变量执行 set
操作时将副作用重放一遍。但是一个程序有这么多变量,这么多副作用,vue3 是如何管理的呢?
depsMap
是一个绑定属性与副作用的 Map
。他的 key 是变量的属性名,他的 value 是属性对应的副作用。
一个属性不止一个副作用,因此 value 是一个 Set
,这样就能存储多个副作用。
由此可见,dep
是 sideEffect
的集合。这样一来,我们就能管理整个对象的副作用了。
record 和 playback
接下来我们要把上一篇的响应式代码魔改一下。
track 的作用是把副作用存储到结构中, trigger 的作用是从结构中找到副作用然后重放。
这2者,一个 写入sideEffect record ,一个 读取sideEffect playback ,是我们实现响应式的基本思路。
下面代码中通过 track 一步一步构造出 depsMap
和 dep
,构建出属性、副作用之间的关系。
当属性发生变更时,再通过 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 const depsMap = new Map ()let p = { price :10 , quantity :2 } let total = 0 function effect ( ) { total = p.price * p.quantity } function track (key:string ) { let dep = depsMap.get(key) if (!dep){ depsMap.set(key,(dep = new Set ())) } dep.add(effect) } function trigger (key:string ) { const dep = depsMap.get(key) if (dep){ dep.forEach((effect:any )=> { effect() }) } } track('quantity' ) effect() total p.quantity = 3 trigger('quantity' ) total
管理多个对象
为了管理更多响应式对象,我们使用一个新的 Map 记录所有的响应式对象。
引入新名词:targetMap
targetMap 是绑定对象和depsMap的 WeakMap
。他的 key 是对象,而 value 是 depsMap。
这么一来,所有的对象都被记录在 targetMap
,每个对象都有自己的 depsMap
,每个 depsMap
都记录着对象的每个属性应该有哪些副作用。
加入 targetMap
之后,代码如下:
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 const targetMap = new WeakMap ()let p = { price :10 , quantity :2 } let total = 0 function effect ( ) { total = p.price * p.quantity } function track (target:any,key:string ) { 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) } function trigger (target:any,key:string ) { const depsMap = targetMap.get(target) if (!depsMap){ return } const dep = depsMap.get(key) if (dep){ dep.forEach((effect:any )=> { effect() }) } } track(p,'quantity' ) effect() total p.quantity = 3 trigger(p,'quantity' ) total
结合 Proxy 拦截实现自动 track 和 trigger
以上面的代码为基础,我们将 Proxy 封装成 reactive 函数,实现“自动”启动 track 和 trigger 。
reactive 的作用是通过 Proxy 构造拦截器,把 track 和 trigger 两个方法放在 get,set
拦截器中调用。
因为执行 effect
时,会执行属性的读取,因此触发 track 将对象添加到 targetMap
,然后构建关联的 depsMap
,再构建 dep
存储当前 effect
。
后来为属性设置新值时,就会触发 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 const targetMap = new WeakMap ()function track (target:any,key:string ) { 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) } function trigger (target:any,key:string ) { const depsMap = targetMap.get(target) if (!depsMap){ return } const dep = depsMap.get(key) if (dep){ dep.forEach((effect:any )=> { effect() }) } } function reactive (target:any ) { const handler:any = { get (target:any,key:string,receiver:any ) { const result = Reflect .get(target,key,receiver) track(target,key) return result }, set (target:any,key:string,value:any,receiver:any ) { const oldVal = target[key] const result = Reflect .set(target,key,value,receiver) if (result && oldVal !== value){ trigger(target,key) } return result } } return new Proxy (target,handler) } let p = { price :10 , quantity :2 } let product = reactive(p)let total = 0 function effect ( ) { total = product.price * product.quantity } effect() total product.quantity = 3 total
限制 track 的执行
我们发现,程序中读取属性是非常普遍的操作,甚至在副作用中也会读取属性。这样很容易形成一个闭环:trigger->track->trigger->track->…
原因是执行 trigger 时,我们是在一个 forEach 循环里,而 track 的作用是往 Set 中添加副作用,这样导致了执行 effect,然后添加 effect,再执行刚刚添加的 effect,再添加 effect,最后递归下去没有出口。
我们下面添加一个控制变量 activeEffect ,让我们的 track 只在首次执行副作用时才被调用。
为此,我们重写了 effect
方法,effect
不再是副作用,而是一个传入副作用的函数,里面不仅管理了 activeEffect
的状态,还调用了副作用本身。
我们还修改了 track 方法,当 activeEffect
有值时,才会往下执行,并且最后 dep
添加的是 activeEffect
。
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 const targetMap = new WeakMap ()let activeEffect:any = null function track (target:any,key:string ) { if (!activeEffect){ return } 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:any,key:string ) { const depsMap = targetMap.get(target) if (!depsMap){ return } const dep = depsMap.get(key) if (dep){ dep.forEach((effect:any )=> { effect() }) } } function reactive (target:any ) { const handler:any = { get (target:any,key:string,receiver:any ) { const result = Reflect .get(target,key,receiver) track(target,key) return result }, set (target:any,key:string,value:any,receiver:any ) { const oldVal = target[key] const result = Reflect .set(target,key,value,receiver) if (result && oldVal !== value){ trigger(target,key) } return result } } return new Proxy (target,handler) } function effect (eff:any ) { activeEffect = eff activeEffect() activeEffect = null } let p = { price :10 , quantity :2 } let product = reactive(p)let total = 0 effect(()=> { total = product.price * product.quantity }) total product.quantity = 3 total
未完持续O(∩_∩)O