vue3 源码学习:reactive 响应式原理

reactive 功能介绍

根据官方的推荐,reactive 通常用于创建响应式对象或者数组,本质上是对原始对象的代理,所以响应式对象和原始对象是不相等的

1<template> 2 {{ state.count }} 3</template> 4 5<script setup> 6 import { reactive } from 'vue' 7 const raw = { 8 count: 0, 9 name: 'hello' 10 } 11 const state = reactive(raw) 12 13 // 响应式对象和原始对象不相等 14 console.log(state === raw) // false 15 console.log(state === reactive(raw)) // true 16</script>

但是 reactive 使用过程中有两个限制

  1. 只对对象类型生效(对象、数组、Map、Set),对原始类型无效(string、number)

  2. 替换或者解构 reactive 对象可能造成响应式丢失,如果需要结构的话需要通过通过 toRefs 保持响应式

    1const state = reactive({ 2 count: 0, 3 name: 'hello', 4}) 5 6// 结构会导致响应式丢失 7const { count, name } = state 8 9// 使用 toRefs 方法保持响应式 10const { count, name } = toRefs(state)

reactive 实现原理

响应式相关的 api 都定义在 reactivity 这个包下面,reactive 入口函数可以看出,非只读的对象通过 createReactiveObject 方法创建响应式变量

1export function reactive(target: object) { 2 // 如果对象是只读的,直接返回对象 3 if (isReadonly(target)) { 4 return target 5 } 6 7 return createReactiveObject( 8 target, 9 false, 10 mutableHandlers, 11 mutableCollectionHandlers, 12 reactiveMap 13 ) 14}

createReactiveObject 方法中

  • 首先根据传入对象 target 做一系列判断,如果不满足条件则直接返回 target,只有 target 是对象类型、没有被 proxy 代理、在缓存 proxyMap 中不存在、符合 targetType 要求才会使用 Proxy 代理
  • 使用 Proxy 代理对象时会根据 targetType 不同使用不同的代理方法,最后把 proxy 后的对象放入缓存并返回即可
1function createReactiveObject( 2 target: Target, 3 isReadonly: boolean, 4 baseHandlers: ProxyHandler<any>, 5 collectionHandlers: ProxyHandler<any>, 6 proxyMap: WeakMap<Target, any> 7) { 8 // 非对象类型直接返回 target 9 if (!isObject(target)) { 10 return target 11 } 12 // 对象已经是 proxy,直接返回 13 if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) { 14 return target 15 } 16 // 在缓存中存在 proxyMap,直接返回缓存对象 17 const existingProxy = proxyMap.get(target) 18 if (existingProxy) { 19 return existingProxy 20 } 21 // 不符合 target 类型要求,直接返回 22 const targetType = getTargetType(target) 23 if (targetType === TargetType.INVALID) { 24 return target 25 } 26 27 // 核心:根据传入对象的不同类型,使用不同的代理方法,再使用 Proxy 代理 28 const proxy = new Proxy( 29 target, 30 targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers 31 ) 32 // 设置缓存并返回代理对象 33 proxyMap.set(target, proxy) 34 return proxy 35}

那么 targetType 是如何被定义的,从 TargetType 枚举可以看出,targetType 分别有三类,在通过 getTargetType 获取对象类型时,如果对象被标记了 skip 属性或者不允许添加新属性,则会被标记为 INVALID。然后 Object 和 Array 类型被划分至 COMMON 普通对象,Map、Set、WeakMap、WeakSet 被划分至集合对象

1const enum TargetType { 2 INVALID = 0, // 非法对象 3 COMMON = 1, // 普通对象 4 COLLECTION = 2, // 集合对象 5} 6 7function targetTypeMap(rawType: string) { 8 switch (rawType) { 9 case 'Object': 10 case 'Array': 11 return TargetType.COMMON 12 case 'Map': 13 case 'Set': 14 case 'WeakMap': 15 case 'WeakSet': 16 return TargetType.COLLECTION 17 default: 18 return TargetType.INVALID 19 } 20} 21 22// 获取对象类型 23function getTargetType(value: Target) { 24 return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) 25 ? TargetType.INVALID 26 : targetTypeMap(toRawType(value)) 27}

再确定了对传入对象 target 类型之后,接下来我们先看普通对象和数组的代理方法定义,根据需要创建的 reactive 类型不同,会区分为如下 4 类

1mutableHandlers // 普通 reactive 代理方法 2shallowReactiveHandlers // 浅响应式 reactive 代理方法 3readonlyHandlers // 只读 reactive 代理方法 4shallowReadonlyHandlers // 浅响应只读 reactive 代理方法

其中 mutableHandlers 定义了完整的 reactive 代理方法,涵盖了对属性的增删改查过程

1export const mutableHandlers: ProxyHandler<object> = { 2 get, 3 set, 4 deleteProperty, 5 has, 6 ownKeys, 7}

get 方法代理

get 代理方法通过 createGetter 方法创建,createGetter 会返回一个 get 函数,从完整的方法函数可以看到,会对类型为普通对象和数组,以及属性 key 不同时有不同的处理方式,具体判断我都写在注释中了

1function createGetter(isReadonly = false, shallow = false) { 2 return function get(target: Target, key: string | symbol, receiver: object) { 3 // 如果属性 key 是 reactive、readonly、shallow,直接返回 4 // 如果 key 是 raw 类型,并且能够从缓存中获取代理结果,直接返回 target 5 if (key === ReactiveFlags.IS_REACTIVE) { 6 return !isReadonly 7 } else if (key === ReactiveFlags.IS_READONLY) { 8 return isReadonly 9 } else if (key === ReactiveFlags.IS_SHALLOW) { 10 return shallow 11 } else if ( 12 key === ReactiveFlags.RAW && 13 receiver === 14 (isReadonly 15 ? shallow 16 ? shallowReadonlyMap 17 : readonlyMap 18 : shallow 19 ? shallowReactiveMap 20 : reactiveMap 21 ).get(target) 22 ) { 23 return target 24 } 25 26 // 在非只读场景下,处理数组和属性 key 是 hasOwnProperty 时的代理结果 27 const targetIsArray = isArray(target) 28 29 if (!isReadonly) { 30 if (targetIsArray && hasOwn(arrayInstrumentations, key)) { 31 return Reflect.get(arrayInstrumentations, key, receiver) 32 } 33 if (key === 'hasOwnProperty') { 34 return hasOwnProperty 35 } 36 } 37 38 // 获取 get 代理结果 39 const res = Reflect.get(target, key, receiver) 40 41 // 处理 key 是 Symbol 类型场景 42 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { 43 return res 44 } 45 46 // 如果对象是非只读,通过 track 方法手机依赖 47 if (!isReadonly) { 48 track(target, TrackOpTypes.GET, key) 49 } 50 51 // 如果对象是浅响应场景,直接返回结果 52 if (shallow) { 53 return res 54 } 55 56 // 如果代理结果是 ref 类型,对于非数组 key 的响应式,返回的是 .value 移除 ref 包裹的结果 57 if (isRef(res)) { 58 return targetIsArray && isIntegerKey(key) ? res : res.value 59 } 60 61 // 如果代理结果是 ref 类型,在非只读情况下递归调用 reactive 方法将整个对象都设置为响应式 62 if (isObject(res)) { 63 return isReadonly ? readonly(res) : reactive(res) 64 } 65 66 return res 67 } 68}

但是对 createGetter 方法进行简化后,可以看到核心部分只有 4 行代码,通过 Reflect.get 获取代理结果,通过 tarck 方法追踪函数的副作用,简化后的 createGetter 方法如下

1function createGetter(isReadonly = false, shallow = false) { 2 const res = Reflect.get(target, key, receiver) 3 4 track(target, TrackOpTypes.GET, key) 5 6 return res 7}

在介绍 track 方法之前,先要说明一下响应式依赖的存储数据结构 targetMap,targetMap 自身是一个 weakMap,键是原始对象,值是 map 实例;map 实例存储的键是原始对象的 key,值是由副作用函数组成的 set 集合。这样就形成了一个 target -> key -> effect 的结构,我们只要通过代理对象 target 和 key 就能够拿到全部的副作用函数

另外为什么 targetMap 使用的是 weakMap 而不是普通 Map,因为 weakMap 是弱引用,当 weakMap 的 key 没有引用时, 垃圾回收机制会将 key 和 value 从内存中移除。当对象 target 没有任何响应式依赖时,说明不需要再对 target 作响应式追踪,及时移除能够避免内存溢出

![](https://notesimgs.oss-cn-shanghai.aliyuncs.com/img/截屏2023-03-12 18.58.37.png)

track 方法中, 当 shouldTrack 为 true,activeEffect 副作用函数存在时才收集依赖,从 targetMap 中根据 target 和 key 获取响应式依赖并交给 trackEffects 处理。在 trackEffects 方法中,在满足条件的情况下,将依赖放入集合中

1export function track(target: object, type: TrackOpTypes, key: unknown) { 2 // 当 shouldTrack 为 true,activeEffect 副作用函数存在时才收集依赖 3 if (shouldTrack && activeEffect) { 4 // 从 targetMap 中根据对象 key 获取对应的副作用函数,获取不到直接创建 5 let depsMap = targetMap.get(target) 6 if (!depsMap) { 7 targetMap.set(target, (depsMap = new Map())) 8 } 9 let dep = depsMap.get(key) 10 if (!dep) { 11 depsMap.set(key, (dep = createDep())) 12 } 13 14 // 将 dep 副作用函数集合交给 trackEffects 执行 15 trackEffects(dep) 16 } 17} 18 19export function trackEffects(dep: Dep) { 20 let shouldTrack = false 21 // 在没有超过依赖递归最大深度的情况下,新添加追踪的依赖才收集 22 if (effectTrackDepth <= maxMarkerBits) { 23 if (!newTracked(dep)) { 24 dep.n |= trackOpBit 25 shouldTrack = !wasTracked(dep) 26 } 27 } else { 28 // 超过依赖递归最大深度,需要追踪依赖 29 shouldTrack = !dep.has(activeEffect!) 30 } 31 32 // 如果应该收集依赖,则将 activeEffect 副作用函数添加到 dep 副作用函数集合中 33 if (shouldTrack) { 34 dep.add(activeEffect!) 35 activeEffect!.deps.push(dep) 36 } 37}

另外要单独说一下对于数组的 get 方法处理,对于数组的操作方法主要分

  • 数组查询方法,不会改变数组长度,比如 includesindexOf 方法
  • 操作数组方法,会改变数组长度,比如 pushpop 等方法

对于这两类操作,在 vue3 中通过直接重写数组方法 createArrayInstrumentations

  • 对于数组查询类方法,会遍历数组并对数组的每一项做响应式追踪,然后先从 this 指向的代理对象查询,查询不到再从 this.raw 指向的原数组查询
  • 而对于操作数组的方法,因为操作过程会读取数组的 length 属性,造成副作用函数的死循环,所以在执行数组操作方法,先禁止 track,操作完成后再恢复
1function createArrayInstrumentations() { 2 const instrumentations: Record<string, Function> = {} 3 // 数组查询方法 4 ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach((key) => { 5 instrumentations[key] = function (this: unknown[], ...args: unknown[]) { 6 const arr = toRaw(this) as any 7 for (let i = 0, l = this.length; i < l; i++) { 8 track(arr, TrackOpTypes.GET, i + '') 9 } 10 // 执行数组查询 11 const res = arr[key](...args) 12 if (res === -1 || res === false) { 13 return arr[key](...args.map(toRaw)) 14 } else { 15 return res 16 } 17 } 18 }) 19 // 数组操作方法 20 ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key) => { 21 instrumentations[key] = function (this: unknown[], ...args: unknown[]) { 22 // 执行操作方法前先停止 track 23 pauseTracking() 24 // 执行数组操作方法 25 const res = (toRaw(this) as any)[key].apply(this, args) 26 // 恢复 track 27 resetTracking() 28 return res 29 } 30 }) 31 return instrumentations 32}

set 方法代理

set 代理方法通过 createSetter 方法创建,createSetter 方法同样会对 readonly、shallow 等场景做处理,下面只列最核心的部分。首先排除掉 target 的原型链也是 proxy 的情况,避免二次触发 setter,再根据 key 是否存在于 target 中,执行新增属性还是修改属性 trigger 方法

1function createSetter(shallow = false) { 2 return function set( 3 target: object, 4 key: string | symbol, 5 value: unknown, 6 receiver: object 7 ): boolean { 8 // 判断 key 是否存在于 target 中 9 const hadKey = 10 isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) 11 // set 方法的执行结果 12 const result = Reflect.set(target, key, value, receiver) 13 14 // target 的原型链也是 proxy 的情况下,通过 Reflect.set 修改原型链上的属性会再次出发 setter 15 // 所以此时就不用触发 trigger 了 16 if (target === toRaw(receiver)) { 17 if (!hadKey) { 18 trigger(target, TriggerOpTypes.ADD, key, value) 19 } else if (hasChanged(value, oldValue)) { 20 trigger(target, TriggerOpTypes.SET, key, value, oldValue) 21 } 22 } 23 return result 24 } 25}

trigger 函数会从 targetMap 中获取响应式依赖集合,也就是需要执行的副作用函数,然后通过 triggerEffects 方法执行获取到的副作用函数

1export function trigger(/*相关参数*/) { 2 const depsMap = targetMap.get(target) 3 // targetMap 中没有说明没有追踪依赖,直接返回 4 if (!depsMap) { 5 return 6 } 7 8 let deps: (Dep | undefined)[] = [] 9 // key 为 length 表示对数组的依赖执行 10 if (key === 'length' && isArray(target)) { 11 const newLength = Number(newValue) 12 depsMap.forEach((dep, key) => { 13 if (key === 'length' || key >= newLength) { 14 deps.push(dep) 15 } 16 }) 17 } else { 18 // 获取对象中的依赖函数 19 if (key !== void 0) { 20 deps.push(depsMap.get(key)) 21 } 22 } 23 24 // 通过 triggerEffects 执行副作用函数 25 if (deps.length === 1) { 26 if (deps[0]) { 27 triggerEffects(deps[0]) 28 } 29 } else { 30 const effects: ReactiveEffect[] = [] 31 for (const dep of deps) { 32 if (dep) { 33 effects.push(...dep) 34 } 35 } 36 triggerEffects(createDep(effects)) 37 } 38}

triggerEffects 函数中,会把传入的副作用函数集合转化为数组统一遍历,然后先执行 computed 类型的副作用函数,因为 computed 执行过程中可能会产生新的副作用函数,最后再执行非 computed 类型的副作用函数,所有副作用函数都通过 triggerEffect 函数执行

triggerEffect 执行过程中会判断副作用函数是否有 scheduler 调度器,有的话直接执行调度器,没有的话直接通过 run 方法执行副作用函数

1export function triggerEffects(dep: Dep | ReactiveEffect[]) { 2 // 将副作用函数转换为函数统一遍历 3 const effects = isArray(dep) ? dep : [...dep] 4 // 首先执行 computed 类型的副作用函数,因为 computed 执行过程中可能会产生新的副作用函数 5 for (const effect of effects) { 6 if (effect.computed) { 7 triggerEffect(effect) 8 } 9 } 10 // 执行非 computed 类型的副作用函数 11 for (const effect of effects) { 12 if (!effect.computed) { 13 triggerEffect(effect) 14 } 15 } 16} 17 18function triggerEffect(effect: ReactiveEffect) { 19 if (effect !== activeEffect || effect.allowRecurse) { 20 if (effect.scheduler) { 21 effect.scheduler() 22 } else { 23 effect.run() 24 } 25 } 26}

deleteProperty、has、ownKeys 方法代理

这三个方法也是在对属性做删除、查询属性、查询属性 key 时,做响应式操作。deleteProperty 方法在删除属性时执行 trigger 执行副作用函数,hasownKeys 查找属性时执行 track 收集副作用函数

1function deleteProperty(target: object, key: string | symbol): boolean { 2 const hadKey = hasOwn(target, key) 3 const oldValue = (target as any)[key] 4 const result = Reflect.deleteProperty(target, key) 5 if (result && hadKey) { 6 trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) 7 } 8 return result 9} 10 11function has(target: object, key: string | symbol): boolean { 12 const result = Reflect.has(target, key) 13 if (!isSymbol(key) || !builtInSymbols.has(key)) { 14 track(target, TrackOpTypes.HAS, key) 15 } 16 return result 17} 18 19function ownKeys(target: object): (string | symbol)[] { 20 track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) 21 return Reflect.ownKeys(target) 22}

reactive 原理回顾

最后我们来整理一下 reactive 方法整体的实现原理,整体一共分为三步

  1. 根据代理对象的类型,通过 Proxy 创建代理
  2. 定义对象操作的不同代理对象,主要是 get 读取操作和 set 编辑操作
  3. get 操作时通过 track 方法收集副作用函数,set 操作时通过 trigger 方法执行副作用函数