Vue 响应式机制使用了观察者模式,数据源作为发布者,更新后需要通知自身的观察者(deps),触发相应的事件。
实现方式上,Vue 2 使用 Object.defineProperty (重写对象属性的 getter 和 setter 方法),Vue 3 使用的是 Proxy,都是针对对象。JavaScript 并不能直接检测到变量的变化,只能劫持对象操作。Object.defineProperty 最大的问题是,代理的是对象上的属性,例如 obj.a 的读取和设置,不能拦截到 obj 的新增或删除属性。Proxy 中做了更完整的代理,常用的有 get(获取属性)、set(设置属性,包括新增和修改)、deleteProperty(删除属性)。
Proxy 基本使用:
let temp = 1
temp = 2 // js 没有提供机制检测这种修改
let val = "aa"
const obj1 = {
a: val,
};
// 代理 obj1 中属性 a
Object.defineProperty(obj1, "a", {
get() {
getterHandler() // do something
// 不能直接获取 obj1.a
// 会再次触发 getter
return val;
},
set(newValue) {
if(val !== newValue){
setterHandler() // do something
// 直接设置 obj1.a
// 会重复触发 setter
val = newValue
};
},
enumerable: true,
configurable: true,
});
const obj2 = {
a: "aa",
};
const proxyObj = new Proxy(obj, {
get(target, key) {
console.log('getter', key)
getterHandler() // do something
return Reflect.get(target, key);
},
// 即使新增 key 也能检测到
// 代理针对的是对象
set(target, key, value) {
// 严格模式下 return falsish 会报错
setterHandler() // do something
return Reflect.set(target, key, value);
},
});
使用 Object.defineProperty,我们操作的就是原数据,为了避免重复触发 getter 和 setter,每个属性需要映射一个值。而 proxy 会返回一个代理对象,开发者操作修改的是代理对象。代理对象本身并不存储值,只是拦截操作做额外处理,最后使用 Reflect 将操作反射到 target 上。上面注释的 do something 就是可以实现响应式的地方,响应式数据需要做额外处理,性能上有损耗,实践中不能无脑使用响应式。这样我们的工作就涉及几步:
- 分析响应式使用的场景。
- 设置响应式数据源。
- 设置响应式数据改变后的回调。
后面两点,实现上要比示例复杂。
先来看一段代码:
const test = ref(22)
console.log('set up', test.value)
setTimeout(() => {
test.value = 33
console.log('value changed', test.value)
}, 1000)
testChange() // 调用方法,
function testChange() {
console.log('inner test', test.value)
}
// 最终结果
// set up 22
// inner test 22
// value changed 33
// 没有调用 testChange
上述代码,定义了一个响应式数据 test。1 秒后会修改 test 的值,响应式数据变更触发更新。接来下调用 testChange 方法,成功打印结果,方法并没有问题。1 秒后 test 更新,没有再次触发 testChange。这里缺少了让响应式数据和更新操作产生联系的关键步骤。
开发角度考虑,将定义响应式和更新操作收集解耦,是很重要的。功能开发,经常是迭代累加的。一个基础数据,后续可能添加各种功能。这里就需要一种机制,检测到当前需要收集的更新操作。
如何做到这一点,Vue 官网有基础示例:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
// 获取位置,追踪依赖
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
// 修改时触发更新
trigger(target, key)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
响应式变量,一定有使用的地方,使用就会触发 getter,Vue 会在 getter 中追踪依赖。随后再次设置值,就会触发 setter,Vue 会在这里触发响应式操作更新页面状态(Vue 称之为副作用,effect)。
流程:1. 运行 effect 方法,方法会使用响应式数据,触发 getter 收集 deps(实际是订阅者,subscriber),就是当前运行的 effect。2. 修改响应数据时,会触发 setter,setter 中通知 deps 主动触发更新操作。3. 更新操作会再次获取数据,触发 getter 重新收集 deps。三步循环执行。
第三步重新收集 dep 很重要,不要以为这和第一步重复了。假设有这样一个 effect:
// 示例伪代码
const effect = () => {
if (dep1) {
do something with dep2
}
}
effect 使用到两个响应式数据 dep1 和 dep2。第一次 dep1 是 true,运算需要使用 dep2 ,此时 effect 同时依赖于 dep1 和 dep2。接着 dep1 变成 false,执行 effect 没有问题。这一次没有用到 dep2,后续再修改 dep2,都不应通知 effect。下一次 dep2 修改时,会重新收集订阅,effect 不在其中,就不应该执行 effect,同时要将 effect 从订阅列表移除。
ref 和 reactive
Vue 中常用的响应式 API 包括 ref 和 reactive 两种,直接看源码实现。
ref
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
// 重复创建拦截
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
// Ref class
class RefImpl<T> {
private _value: T // 保存的值
private _rawValue: T // 保存的原始值,响应式变量的 target
public dep?: Dep = undefined // 订阅列表
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// Object 会转成 reactive
this._value = __v_isShallow ? value : toReactive(value)
}
// get 关键字定义 getter
get value() {
// 收集依赖
trackRefValue(this)
// 返回值
return this._value
}
// set 关键字定义 setter
// value 是属性名
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 使用 Object.is 判断是否修改值
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
// 判断是否 shallowRef,shallowRef 不做深层递归代理
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 触发更新
triggerRefValue(this, newVal)
}
}
}
export function trackRefValue(ref: RefBase<any>) {
// 当前变量是否需要收集,是否有运行中的 effect
// 高阶组件中,有父子通讯,可能会造成嵌套
// 使用 shouldTrack 手动阻止
if (shouldTrack && activeEffect) {
// TODO 返回原始值
// 判断是否有 __v_raw,ref 没有看到这个属性
// 后续看为什么这里需要 toRaw
ref = toRaw(ref)
if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
// 默认创建一个新的 dep
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
ref 整体功能和示例代码差不多,setter 增加 lazy 处理。track 和 trigger 是对 trackEffects 和 triggerEffects 的封装处理,通用方法后续看。
reactive
// 会将生成的 reactive 全部用 WeakMap 保存起来
// key 是 target 被代理对象
// proxy 可以获取到当前访问的 target
// 找到对应的 reactive 对象
export const reactiveMap = new WeakMap<Target, any>()
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false, // 是否只读,闭包保存
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 简单类型不能做响应式处理
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
// 判断是否为合法的响应式数据源类型
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
// 判断数据源类型,适用不同的 handlers
// Object Array 使用 baseHandlers
// Map Set WeakMap WeakSet 使用 collectionHandlers
// 这两类数据访问自身属性形式不一样,做了区分
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 保存 map
proxyMap.set(target, proxy)
return proxy
}
接下来是重点,代理对象的 getter,setter 方法。Object 和 Array 可以直接设置、添加属性,例如 arr[0] = 1,需要拦截 gettter 和 setter。Map 和 Set 不一样,操作都是调用的实例方法,例如 map.delete(key)、map.set(key, value),实例方法都会触发 getter,通过方法名进行拦截。先看最常用的 Object 和 Array:
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
// has 和 ownKeys 比较简单,就是代理操作,同时追踪收集依赖
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
// track 后面两个参数用于 dev 展示调试信息
// 整个 track 方法后面再看
track(target, TrackOpTypes.HAS, key)
}
return result
}
function ownKeys(target: object): (string | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
// deleteProperty 删除属性,需要触发更新
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
接下来看 getter,getter 是 createGetter 返回的一个闭包方法:
// 所有 reacrive 用的都是同一个 get 方法
// 避免每个对象新建一个闭包占用内存
// 通过闭包也保存了共有的 isReadonly shallow 信息
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 特殊属性的代理
// 体现 proxy 特点,可以访问 target 中没有的属性
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
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)
// 不需要代理的属性,有一些 builtin 方法,也需要获取属性对比
// 这部分不涉及视图更新
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
// 追踪数据
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
// 浅层代理,直接返回,无需递归
return res
}
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)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
// 默认将内部访问数据,也会转换成响应式
// 也就是获取时再转换,定义时不会转换
// reactive 中没有使用的属性不会转换成响应式
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
// 重写的数组方法
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
// 查找时,可能会有响应式数据,在原数据中是查找不到的
// 需要做额外处理
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
// 改变数组长度的方法,内部可能会获取数组长度,形成 infinite loops
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 暂停依赖收集
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
// 恢复
resetTracking()
return res
}
})
return instrumentations
}
setter:
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
// readonly 属性修改拦截,readonly 数据不能直接修改
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!shallow && !isReadonly(value)) {
// 展平数据
if (!isShallow(value)) {
value = toRaw(value)
oldValue = toRaw(oldValue)
}
// 子元素是响应式数据,触发自身的更新
// shallow 数据无需继续递归
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) {
// 不存在为新增
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 存在为修改
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
以上就是 Vue 在属性劫持中做的额外处理。
Map、Set 对应的方法在 collectionHandlers.ts 中,只劫持了 getter,getter 中对调用方法做了处理,值得注意的是 keys()、values() 等方法,需要手动创建迭代器。
track 和 trigger 逻辑
ref 中使用的是 trackEffects 和 triggerEffects,reactive 则使用了 track 和 trigger 方法。来到 effect.ts 文件中,会发现 track 和 trigger 是对 trackEffects 和 triggerEffects 方法的二次封装:
export function track(target: object, type: TrackOpTypes, key: unknown) {
// shouldTrack 标识是否需要追踪,如上面所示
// 有些情况需要阻止响应式,比如,array 一些 builtin 方法
// 本身需要获取 length,可能会触发 loop
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// Dep 本身是一个 Set 增加了两个标识位
// w 标识是否已被追踪
// n 标识新增的 dep
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
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) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
// 数组长度修改,超出长度的都需要舍弃
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
// dep 本质是一个 Set,保存 effects
// 展开后就是 effects
// w & n 的标志已经没有用了
// 这里的一定是有更新的
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
再看一下基础的方法:
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 默认 false 重复拦截
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
// 新增的依赖需要加入当前 effect
// 如果已经添加过,即 wasTracked 无需多次添加
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
// 添加依赖
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
Object.assign(
{
effect: activeEffect!
},
debuggerEventExtraInfo
)
)
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
总结
整理一下,reactive 和 ref 的响应式逻辑。
调用方法,首先会创建一个代理对象,代理对数据的操作。ref 和 reactive 转换方式略有区别,ref 使用 Vue 自己封装的 RefImpl 类做代理,其中实现了 getter 和 setter。ref 通常适用于简单数据类型,没有做额外的嵌套判断,如果传入对象,会转成 reactive 处理。reactive 中维护了一个 WeakMap 对象 reactiveMap,用来避免嵌套造成的 infinity loop(和深拷贝实现类似)。
在 reactive 中,有几个属性(ReactiveFlags)体现了 proxy 的特点。如果打印 reactive 返回结果,就是一个普通的 proxy 对象,只有 target、handlers 两个属性,ReactiveFlags 对应的属性是通过拦截计算得到的。
reactive 在 track 处理中,会创建 target -> key -> dep 的链条,dep 中有保存相关的 effects。这里有两种 map,KeyToDepMap 保存 target 属性到 dep 的映射关系,targetMap 存放 target 到 KeyToDepMap 的映射。targetMap 使用 WeakMap,某个 target 被 GC 清理后,WeakMap 中的键引用将不存在,相应值(KeyToDepMap)没有额外引用,也会被清理。dep 是一个 Set,track 过程会将当前正在运行的 effect(activeEffect)添加进去。trigger 时,会依次调用 dep 中存放的 effect。ref 的 track 处理更加简单,dep 是在 RefImpl 实例内部维护的,track 时直接将 activeEffect 添加进 dep 即可。
trigger 过程最终就是要执行 effect。ref 调用保存在 dep 中 effects 即可,reactive 需要根据之前存储的 target -> key -> dep 查找,找到所有的 effects 执行即可。
这个就是典型的观察者模式,每个响应式变量是一个主题,内部通过 deps 维护一个观察者(effects)列表,每次发生变化,通知 effects(调用 effect 的 run 方法)。
WeakMap、WeakSet 使用的是弱引用,不会阻止对象被 GC 清理,合法的数据只能是对象或者符号(Symbol)。WeakMap 的键被清理后,如果值没有别的引用,也会被清理。
const weakTest = new WeakMap() let obj = { text: 'pick a place' } weakTest.set(obj, true) // 此时 weakTest 有一个 obj 为 key 的键值对 console.log(weakTest); // 清除原有对象的引用 // 刚清除引用,不会有变化 // 等到下一轮 GC 完成,对象被清除后 // 会发现 weakTest 没有元素了 // chrome 可以在 performance 手动触发 GC obj = null
到这里响应式数据定义已经很清晰了,还有一个问题就是 Effect 做了什么。可以看到收集订阅时都判断了,是否有激活的 effect。