Vue响应式原理(上)
前言Vue 实现响应式的原理中有两个最重要的步骤依赖收集和派发更新。接下来我们将围绕这两点进行完善和优化以实现响应式功能。具体实现基础版依赖收集和派发更新letprice5letquantity2lettotal0letdepnewSet()leteffect(){totalprice*quantity}functiontrack(){dep.add(effect)}functiontrigger(){dep.forEach(effecteffect())}track()effect()先来简单解释一下核心概念我们将使用到响应式变量的函数称为副作用函数 (effect)因为它改变了程序的状态。依赖收集 (Track)即记录下哪些副作用函数正在使用该响应式变量派发更新 (Trigger)当响应式变量改变时将所有关联的副作用函数从集合中取出并重新执行。在代码实现中我们使用track()函数来进行依赖收集使用trigger()函数来进行派发更新。完善收集和查找功能首先我们将针对对象类型的变量来设计函数。原始类型变量只需要修改值而对象类型变量较复杂所以我们优先解决复杂的类型。对象型变量具有多个键值对而每个键值对都可能对应多个副作用函数。要建立精确的响应关系我们需要一套从“对象” - “属性” - “副作用函数集合”的映射结构。这启发了我们使用嵌套的 Map 类型数据来存储这种关系。// 使用WeakMap的原因// 当target对象在业务逻辑中被销毁被垃圾回收时WeakMap会自动清理避免内存泄漏consttargetMapnewWeakMap()functiontrack(target,key){letdepsMaptargetMap.get(target)if(!depsMap){// depsMap的作用是根据对象的属性名查找对应的依赖集合// 属性名通常是字符串或者Symbol类型Map能够高效抽离各种类型的键值对映射且这里属性名不需要弱引用targetMap.set(target,(depsMapnewMap()))}letdepdepsMap.get(key)if(!dep){// dep的作用是存储effect而一个副作用函数effect不应该被重复收集// set可以自动去重避免重复收集depsMap.set(key,(depnewSet()))}dep.add(effect)}functiontrigger(target,key){constdepsMaptargetMap.get(target)if(!depsMap)returndepdepsMap.get(key)if(dep){dep.forEach(effecteffect())}}letproduct{price:5,quantity:3}lettotal0leteffect(){totalproduct.price*product.quantity}track(product,quantity)track(product,price)effect()在代码中我们使用targetMap来存储不同对象的引用其对应的值是该对象的属性地图depsMap。在depsMap中我们进一步以属性名为键对应存储该属性的副作用函数集合dep。dep本质上是一个 Set存放着所有依赖该属性的副作用函数。targetMap使用WeakMap类型是因为当target对象在业务逻辑中被销毁时WeakMap不会阻止垃圾回收从而自动清理相关记录避免内存泄漏。depsMap用于根据对象的属性名查找对应的依赖集合。属性名通常是字符串Map能够高效建立这种键值对映射。dep使用Set存储effect是因为同一个副作用函数不应该被重复收集Set能够自动去重。以下是官方所给出的模型图。实现自动更新在这一阶段虽然实现了逻辑但每次修改数据都得手动调用trigger()这显然不符合 Vue “数据驱动”的自动化体验。我们要做的是让数据在被读取时自动进行 track在修改时自动运行 trigger。实现这一功能的核心武器是Proxy 代理对象。Vue 3 舍弃了Object.defineProperty转而使用Proxy它可以直接拦截对象的基本操作。// 1. 核心仓库用于存储所有对象的依赖consttargetMapnewWeakMap()// 2. 追踪函数负责把 effect 存入账本functiontrack(target,key){letdepsMaptargetMap.get(target)if(!depsMap){targetMap.set(target,(depsMapnewMap()))}letdepdepsMap.get(key)if(!dep){depsMap.set(key,(depnewSet()))}// 【重点】直接把当前的副作用函数存进去dep.add(effect)}// 3. 触发函数负责把账本里的函数翻出来执行functiontrigger(target,key){constdepsMaptargetMap.get(target)if(!depsMap)returnconstdepdepsMap.get(key)if(dep){// 【重点】挨个执行之前存进去的任务dep.forEach(effecteffect())}}// 4. 响应式转换将普通对象包装成 Proxyfunctionreactive(target){consthandler{get(target,key,receiver){// 在读取属性时悄悄进行依赖收集track(target,key)returnReflect.get(target,key,receiver)},set(target,key,value,receiver){letoldValuetarget[key]letresultReflect.set(target,key,value,receiver)// 在值发生变化时自动触发更新if(oldValue!value){trigger(target,key)}returnresult}}returnnewProxy(target,handler)}当前的逻辑非常精妙在get和set中使用Reflect而不是target[key]是为了确保在对象有继承关系时this指向依然正确。自动化的闭环effect()首次运行 - 触发 Proxy 的get-track()将effect存入 Set - 未来执行product.price xx时 - 触发 Proxy 的set-trigger()从 Set取出effect并运行。更新完成