在正文开始之前,先简述一下响应式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
的分析。