在正文开始之前,先简述一下响应式Proxy
的原理。
原理简述
Proxy
是ES6
新增的对象,可以对指定对象做一层代理。
我们通过Proxy
创建的对象,无非会经过2个步骤,达成我们的响应式需求。
- 收集
watch
函数中的依赖。 - 改变后再次调用
watch
函数。
如何收集依赖
在对象通过Proxy
代理后,我们就可以在读取对象的属性时加上一层拦截,通常形式为:
const p = new Proxy({}, { get(target, key, receiver) { // 拦截 }});
在get
拦截方法中,我们即可以拿到对象本身target
,读取的属性key
,和调用者receiver
(就是p
对象),在这里我们就能够获取当前访问的属性key
。
通常我们会在方法里访问代理后的对象:
function fn(){ console.log(p.value);}fn();
当我们执行了fn
函数后,我们就会触发我们的get
拦截,只需要在get
拦截中记录下当前执行的函数,就可以建立一个key => fn
的映射,后续可以在属性值发生改变后再次调用fn
函数。
所以比较疑惑的点是如何在执行我们的get
拦截的同时,还能获取到是哪一个函数调用了这个代理对象。
在Vue3
的实现中,是使用了一个effect
函数来包装我们自己的函数。
effect(()=>{ console.log(p.value)})
为的就是能将调用了代理对象的函数保存下来。
let activeEffect;function effect(fn){ activeEffect = fn; fn(); // 执行 activeEffect = null;}// ...get(target, key, receiver) { // get拦截中访问全局的activeEffect,就是当前调用的函数 // key => activeEffect}
在get
拦截中还有一个需要注意的点,如果我们需要代理的对象是数组,那么在调用如push
、pop
、includes
等大部分数组方法时,其实都会触发get
拦截,这些方法都会访问数组的length
属性。
触发Watch函数
我们会在值修改后触发保存下来的key => fn
映射的函数。set
拦截会在设置属性值的时触发。
const p = new Proxy({}, { set(target, key, value, receiver) { // 取出key对应的fn来执行 }});
其他的拦截方式
除去我们读取属性时的get
拦截,还需要在其他操作中收集依赖,完善响应式的功能。
has
,in
操作符拦截。ownKeys
- 拦截
Object.getOwnPropertyNames()
。 - 拦截
Object.getOwnPropertySymbols()
。 - 拦截
Object.keys()
。 - 拦截
Reflect.ownKeys()
。
- 拦截
除去设置属性的set
拦截来触发依赖函数,还需要在删除属性时也触发。
deleteProperty
,删除属性时拦截。
除去普通对象和数组的代理,还有一个难点是Map
和Set
对象的代理。
详细的原理实现可以我之前的链接,本文中就不再实现了。
接下来进入正文部分。
源码浅析
Vue3是Monorepo
,响应式的包reacitvity
是单独的一个包。
reactivity
受了以上3个包的启发,刚好我也拜读过observer-util
的源码,reactivity
相对“前辈”做了很多巧妙的改进和功能的增强。
- 增加了
shallow
模式,只有第一层值为响应式。 - 增加了
readonly
模式,不会收集依赖,不能修改。 - 增加了
ref
对象。
文件结构
├── baseHandlers.ts├── collectionHandlers.ts├── computed.ts├── effect.ts├── index.ts├── operations.ts├── reactive.ts└── ref.ts
baseHandlers
和collectionHandlers
为功能的主要实现文件,也就是Proxy
对象对应的拦截器函数,effect
为观察者函数文件。
本文主要分析的也是这3部分。
对象数据结构
Target
类型为需要Proxy
的原始对象,上面定义了4个内部属性。
export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any}
targetMap
为内部保存收集的依赖函数的一个WeakMap
。
type Dep = Set<ReactiveEffect>type KeyToDepMap = Map<any, Dep>const targetMap = new WeakMap<any, KeyToDepMap>()
它的键名是未经过Proxy
响应式操作的原始对象,值为key => Set<依赖函数>
的Map
。
我们会通过targetMap
获取当前对象对应的key => Set<依赖函数>
的Map
,从中取出key
对应的所有依赖函数,然后在值发生改变后调用它们。
以下4个Map
是内部记录原始对象Target
到reactive
或readonly
后对象的映射关系。
export const reactiveMap = new WeakMap<Target, any>()export const shallowReactiveMap = new WeakMap<Target, any>()export const readonlyMap = new WeakMap<Target, any>()export const shallowReadonlyMap = new WeakMap<Target, any>()
baseHandlers
baseHandlers
这个文件里主要是创建了针对普通对象,数组的Proxy
拦截器函数。
先看收集依赖的get
拦截器。
get
function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { // Target 内部键名并没有储存在对象上,而是通过get拦截闭包的返回 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { // target就是raw value,前提是receiver和raw => proxy里的对象一样 return target } const targetIsArray = isArray(target) // 针对数组的特殊处理 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) // 忽略内置symbol和non-trackable键 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res } // readonly不能改不用追踪 if (!isReadonly) { // 收集依赖 track(target, TrackOpTypes.GET, key) } // shallow响应式直接返回结果,不对嵌套对象再做响应式 if (shallow) { return res } // ref的处理 if (isRef(res)) { // ref unwrapping - does not apply for Array + integer key. const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } // 如果值是对象,延迟转换对象 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) } return res }}
在get
拦截器中,首先是一个巧妙的处理返回ReactiveFlags
对应的值,不需要将它对应的值真正的赋值在对象上,接着会对数组做特殊的处理,收集依赖的函数为track
,它定义在effect.ts
中,在后文会分析这一模块。如果返回的值是对象,则会延迟转换对象。
set
function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { let oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) oldValue = toRaw(oldValue) // 如果旧值是ref的情况时,ref内部也有set拦截, if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else { // in shallow mode, objects are set as-is regardless of reactive or not } // 数组判断判断索引是否存在,对象判断是否有key const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { if (!hadKey) { // 无key ADD trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 有key SET trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result }}
set
拦截器里主要是判断设置的key
是否存在,然后分2种参数去trigger
,trigger
函数为触发收集effect
函数的方法,同样定义在effect.ts
中,这里先暂且不提。
ownKeys
function ownKeys(target: object): (string | symbol)[] { // 对于数组来说key是length,对象的话是ITERATE_KEY只作为一个key的标识符 track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) return Reflect.ownKeys(target)}
ownKeys
拦截器同样是收集依赖,需要注意的是传入的key
参数,在target
为数组的时候key
为length
,对象的时候key
为ITERATE_KEY
,ITERATE_KEY
仅为一个symbol
值的标识符,后续会通过这个值来取到对应的effect
函数,实际是不存在这个key
的。
effect
本文的原理简述中提到,如果我们想要知道对象被哪一个函数调用了,需要将函数放入我们自己的运行函数中来调用。实际代码中我们是将传入effect
方法的函数做了一层新的包装,它的类型为ReactiveEffect
。
数据结构
export interface ReactiveEffect<T = any> { (): T _isEffect: true id: number active: boolean // 是否有效 raw: () => T // 原始函数 deps: Array<Dep> // 依赖了该effect的key所对应的保存effect的Set options: ReactiveEffectOptions allowRecurse: boolean}
其中比较重要的字段为deps
,如果我们在执行该effectFn
函数收集依赖时,得到了如下的依赖结构:
{ "key1": [effectFn] // Set "key2": [effectFn] // Set}
那么我们的ReactiveEffect
方法effectFn
的deps
属性保存的值就是这2个key
所对应的Set
。
export function effect<T = any>( fn: () => T, // 传入的函数 options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) // 创建ReactiveEffect if (!options.lazy) { effect() // 执行ReactiveEffect } return effect}
effect
函数中通过createReactiveEffect
创建了ReactiveEffect
。
function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (!effect.active) { return fn() } if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) // 将activeEffect赋值为当前effect activeEffect = effect // 执行函数,对应的拦截器可以通过activeEffect保存对应的effect return fn() } finally { effectStack.pop() resetTracking() // 重置activeEffect activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !!options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect}
在执行effect
函数前,先将函数保存到全局变量activeEffect
中,这样在函数执行的同时,对应的拦截器在收集依赖的时候就能知道当前是哪一个函数在执行。
cleanup
cleanup
方法清除依赖关系。
function cleanup(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 }}
上文提到了deps
属性的结构,保存的是依赖了effectFn
的Set
,遍历它们,将effectFn
从所有的Set
中删除。
track
track
方法收集依赖,功能非常简单,将activeEffect
添加进Dep
。
export function track(target: object, type: TrackOpTypes, key: unknown) { if (!shouldTrack || activeEffect === undefined) { return } let depsMap = targetMap.get(target) // 初始化 target => Map<key,Dep> if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) // 初始化 key => Set<Effect> if (!dep) { depsMap.set(key, (dep = new Set())) } // 当前key对应的Set中不存在activeEffect if (!dep.has(activeEffect)) { dep.add(activeEffect) // 添加进Set activeEffect.deps.push(dep) // 同时添加进Effect的deps if (__DEV__ && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } }}
Trigger
trigger
方法执行ReactiveEffect
,内部会做一些类型判断,比如TriggerOpTypes.CLEAR
只存在于Map
和Set
。
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown>) { const depsMap = targetMap.get(target) if (!depsMap) { return } // 将需要执行的effect都拷贝到effects Set中 const effects = new Set<ReactiveEffect>() const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || effect.allowRecurse) { effects.add(effect) } }) } } // CLEAR类型存在于Map和Set的collectionHandlers中 if (type === TriggerOpTypes.CLEAR) { // Map的forEach第一个参数是值,也就是key对应对Dep Set depsMap.forEach(add) } else if (key === 'length' && isArray(target)) { // 数组 depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { add(dep) } }) } else { // key !== undefined SET | ADD | DELETE if (key !== void 0) { // 只加入当前key的effect函数 add(depsMap.get(key)) } // ITERATE_KEY是一个内置的标识变量 ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes // 新索引 => length 改变 add(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { add(depsMap.get(ITERATE_KEY)) } break } } const run = (effect: ReactiveEffect) => { if (__DEV__ && effect.options.onTrigger) { effect.options.onTrigger({ effect, target, key, type, newValue, oldValue, oldTarget }) } if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } // 执行 effects.forEach(run)}
数组的特殊处理
虽然使用了Proxy
,但是数组方法还是需要特殊处理,避免一些边界情况,它们并没有重写数组方法。
includes, indexOf,lastIndexOf
这3个方法的特殊处理是为了同时能够判断是否存在响应式数据。
const method = Array.prototype[key] as any arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) { const arr = toRaw(this) for (let i = 0, l = this.length; i < l; i++) { // 将每一个下标收集为依赖 track(arr, TrackOpTypes.GET, i + '') } // 先用当前参数执行 const res = method.apply(arr, args) if (res === -1 || res === false) { // 如果没有结果,将参数转为raw值再执行 return method.apply(arr, args.map(toRaw)) } else { return res } }})
为了确保响应式的值和非响应式的值都可以被判断,所以可能会遍历两次。
避免循环依赖
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { const method = Array.prototype[key] as any arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) { pauseTracking() const res = method.apply(this, args) resetTracking() return res }})
数组的方法基本都会隐式的依赖lengh
属性,在某些情况可能会出现循环依赖(#2137)。
总结
以上为Vue 3响应式的对象和数组拦截的源码浅析,本文只简单分析了baseHandlers
中重要的拦截器,后续会带来collectionHandlers
的分析。