Vue响应式原理(上)
前言
Vue 实现响应式的原理中有两个最重要的步骤:依赖收集和派发更新。接下来我们将围绕这两点进行完善和优化,以实现响应式功能。
具体实现
基础版依赖收集和派发更新
letprice=5letquantity=2lettotal=0letdep=newSet()leteffect=()=>{total=price*quantity}functiontrack(){dep.add(effect)}functiontrigger(){dep.forEach(effect=>effect())}track()effect()先来简单解释一下核心概念:我们将使用到响应式变量的函数称为副作用函数 (effect),因为它改变了程序的状态。依赖收集 (Track),即记录下哪些副作用函数正在使用该响应式变量;派发更新 (Trigger),当响应式变量改变时,将所有关联的副作用函数从集合中取出并重新执行。
在代码实现中,我们使用track()函数来进行依赖收集,使用trigger()函数来进行派发更新。
完善收集和查找功能
首先,我们将针对对象类型的变量来设计函数。原始类型变量只需要修改值,而对象类型变量较复杂,所以我们优先解决复杂的类型。
对象型变量具有多个键值对,而每个键值对都可能对应多个副作用函数。要建立精确的响应关系,我们需要一套从“对象” -> “属性” -> “副作用函数集合”的映射结构。这启发了我们使用嵌套的 Map 类型数据来存储这种关系。
// 使用WeakMap的原因// 当target对象在业务逻辑中被销毁,被垃圾回收时,WeakMap会自动清理,避免内存泄漏consttargetMap=newWeakMap()functiontrack(target,key){letdepsMap=targetMap.get(target)if(!depsMap){// depsMap的作用是根据对象的属性名查找对应的依赖集合// 属性名通常是字符串或者Symbol类型,Map能够高效抽离各种类型的键值对映射,且这里属性名不需要弱引用targetMap.set(target,(depsMap=newMap()))}letdep=depsMap.get(key)if(!dep){// dep的作用是存储effect,而一个副作用函数(effect)不应该被重复收集// set可以自动去重,避免重复收集depsMap.set(key,(dep=newSet()))}dep.add(effect)}functiontrigger(target,key){constdepsMap=targetMap.get(target)if(!depsMap)returndep=depsMap.get(key)if(dep){dep.forEach(effect=>effect())}}letproduct={price:5,quantity:3}lettotal=0leteffect=()=>{total=product.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. 核心仓库:用于存储所有对象的依赖consttargetMap=newWeakMap()// 2. 追踪函数:负责把 effect 存入账本functiontrack(target,key){letdepsMap=targetMap.get(target)if(!depsMap){targetMap.set(target,(depsMap=newMap()))}letdep=depsMap.get(key)if(!dep){depsMap.set(key,(dep=newSet()))}// 【重点】直接把当前的副作用函数存进去dep.add(effect)}// 3. 触发函数:负责把账本里的函数翻出来执行functiontrigger(target,key){constdepsMap=targetMap.get(target)if(!depsMap)returnconstdep=depsMap.get(key)if(dep){// 【重点】挨个执行之前存进去的任务dep.forEach(effect=>effect())}}// 4. 响应式转换:将普通对象包装成 Proxyfunctionreactive(target){consthandler={get(target,key,receiver){// 在读取属性时,悄悄进行依赖收集track(target,key)returnReflect.get(target,key,receiver)},set(target,key,value,receiver){letoldValue=target[key]letresult=Reflect.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并运行。更新完成!
