本篇文章我们将阅读 Vue3 最新源码的 reactivity 部分,了解几个 vue 响应式核心 API 的实现。
- 建议大家先阅读我贴出的源码(我添加了部分中文注释帮助大家理解),自己思考其实现原理
- 然后看下我给出的要点,才疏学浅,难免有所疏漏,请大家多多补充
话不多说,我们马上进入正题。
Ref
ref 和 shallowRef 的实现依赖下面这个类型, 实际上他们就是返回一个 RefImpl 实例,如果本身就是 RefImpll 实例了就直接返回。 shallowRef 只是实例化的入参不同, 控制只对对象第一层实现响应式。
- 可以看到, RefImpl 的 _value 属性其实是由 toReactive 得来的。我们先按住不表。
- 在 getter 中调用 dep.trace() 收集 active link
- 在 setter 中调用 dep.trigger() 触发订阅的更新
/**
* @internal
*/
class RefImpl<T = any> {
_value: T
private _rawValue: T
dep: Dep = new Dep()
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
constructor(value: T, isShallow : boolean) {
this._rawValue = isShallow ? value : toRaw(value)
this._value = isShallow ? value : toReactive(value)
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
get value() {
if (__DEV__) {
this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
} else {
this.dep.track()
}
return this._value
}
set value(newValue) {
const oldValue = this._rawValue
const useDirectValue =
this[ReactiveFlags.IS_SHALLOW] ||
isShallow(newValue) ||
isReadonly(newValue)
newValue = useDirectValue ? newValue : toRaw(newValue)
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
if (__DEV__) {
this.dep.trigger({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
})
} else {
this.dep.trigger()
}
}
}
}
与 RefImpl 的区别在于他没有自己的 Dep ,而是依赖于从原本 object 上获取 dep
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly [ReactiveFlags.IS_REF] = true
public _value: T[K] = undefined!
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K],
) {}
get value() {
const val = this._object[this._key]
return (this._value = val === undefined ? this._defaultValue! : val)
}
set value(newVal) {
this._object[this._key] = newVal
}
get dep(): Dep | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}
Dep 类实际上是一个双向链表。有收集依赖,依次(反向)触发的功能
class Dep {
version = 0 // 当前依赖的版本号,用于优化依赖变更检测
activeLink?: Link = undefined // 当前 dep 与活跃 effect 的链接(依赖关系)
subs?: Link = undefined // 订阅者链表的尾部(双向链表,指向最后一个 effect/computed)
subsHead?: Link // 订阅者链表的头部(仅开发环境,用于 onTrigger 钩子顺序调用)
map?: KeyToDepMap = undefined // 用于对象属性依赖清理的映射表
key?: unknown = undefined // 当前 dep 关联的属性 key
sc: number = 0 // 订阅者计数器
readonly __v_skip = true // 内部标记,跳过响应式处理
constructor(public computed?: ComputedRefImpl | undefined) {
// 构造函数,computed 仅用于 computed ref
if (__DEV__) {
this.subsHead = undefined
}
}
// 依赖收集:将当前活跃的 effect/computed 加入依赖链表
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
// ...见源码逻辑...
}
// 依赖触发:当响应式数据变更时,通知所有订阅者
trigger(debugInfo?: DebuggerEventExtraInfo): void {
this.version++
globalVersion++
this.notify(debugInfo)
}
// 通知所有订阅者(effect/computed),依次执行响应
notify(debugInfo?: DebuggerEventExtraInfo): void {
startBatch()
try {
// 开发环境下,先顺序调用 onTrigger 钩子
for (let head = this.subsHead; head; head = head.nextSub) {
// ...
}
// 逆序遍历订阅者链表,依次调用 notify
for (let link = this.subs; link; link = link.prevSub) {
if (link.sub.notify()) {
// 如果是 computed,还要递归通知其依赖
(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
endBatch()
}
}
}
Reactive
reactive 是对象进行代理,实现响应式的过程。Ref 在拓展了原始类型的响应式之外,内部也是使用 toReactive 实现深层的响应式
- reactive & shallowReactive 和 ref 类似,不过这里是通过传入不同的 proxy handler ,调用 createReactiveObject 实现区分
- createReactiveObject 中跟据不同的数据类型传入不同 proxy Handler 在 handler 中拦截 get set has 等属性操作
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
if (!isObject(target)) {
if (__DEV__) {
warn(
`value cannot be made ${isReadonly ? 'readonly' : '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
}
// only specific value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}
以 reactive 类型的非 collection(set,map) handler 为例。对比 ref 的依赖收集来看
- ref 的依赖收集 dep 实例是挂载在 RefImpl 实例上的 ( this.dep.trigger )
- Reactive handler 的 dep 实例是通过 WeakMap (直接传入key 调用 trigger / track函数)
// 可变响应式对象的 Proxy handler,继承自 BaseReactiveHandler
class MutableReactiveHandler extends BaseReactiveHandler {
constructor(isShallow = false) {
super(false, isShallow) // false 表示不是只读,isShallow 是否浅层响应
}
// 拦截属性设置
set(target, key, value, receiver): boolean {
let oldValue = target[key]
if (!this._isShallow) {
// 非浅层模式下,处理只读和响应式解包
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
// 如果原值是 ref,新值不是 ref,直接赋值到 ref.value
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false // 只读 ref 不可赋值
} else {
oldValue.value = value // 赋值到 ref.value
return true
}
}
} else {
// 浅层模式下,直接赋值,不做响应式处理
}
// 判断属性是新增还是修改
const hadKey = isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 通过 Reflect 设置属性
const result = Reflect.set(
target,
key,
value,
isRef(target) ? target : receiver,
)
// 只在本体上触发依赖
if (target === toRaw(receiver)) {
if (!hadKey) {
// 新增属性,触发 ADD
trigger (target, TriggerOpTypes . ADD , key, value)
} else if (hasChanged(value, oldValue)) {
// 修改属性,触发 SET
trigger (target, TriggerOpTypes . SET , key, value, oldValue)
}
}
return result
}
// 拦截属性删除
deleteProperty(target, key): boolean {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
// 删除成功且原本有该属性,触发 DELETE
trigger (target, TriggerOpTypes . DELETE , key, undefined , oldValue)
}
return result
}
// 拦截 in 操作符
has(target, key): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
// 依赖收集 HAS 操作
track (target, TrackOpTypes . HAS , key)
}
return result
}
// 拦截 Object.keys/Object.getOwnPropertyNames 等
ownKeys(target): (string | symbol)[] {
// 依赖收集 ITERATE 操作
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY,
)
return Reflect.ownKeys(target)
}
}
- export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap() 每一个 object 对应一个 DepMap
- const depsMap = targetMap.get(target) DepMap 中以属性层级保存每一个 Dep 实例
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
globalVersion++
return
}
const run = (dep: Dep | undefined) => {
if (dep) {
if (__DEV__) {
dep.trigger({
target,
type,
key,
newValue,
oldValue,
oldTarget,
})
} else {
dep.trigger()
}
}
}
startBatch()
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(run)
} else {
const targetIsArray = isArray(target)
const isArrayIndex = targetIsArray && isIntegerKey(key)
if (targetIsArray && key === 'length') {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (
key === 'length' ||
key === ARRAY_ITERATE_KEY ||
(!isSymbol(key) && key >= newLength)
) {
run(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0 || depsMap.has(void 0)) {
run(depsMap.get(key))
}
// schedule ARRAY_ITERATE for any numeric key change (length is handled above)
if (isArrayIndex) {
run(depsMap.get(ARRAY_ITERATE_KEY))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isArrayIndex) {
// new index added to array -> length changes
run(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
run(depsMap.get(ITERATE_KEY))
}
break
}
}
}
endBatch()
}
- myRef(RefImpl 实例)有一个 dep,负责 .value 的依赖。
- myRef.value(reactive 对象)每个属性也有自己的 dep,负责各自属性的依赖。
- 这两套 dep 互不干扰,分别管理不同层级的响应式依赖。
Computed
compoted 函数的类型声明有两个,这叫做类型重载,允许跟据传入的参数不同而使用不同的声明
- 我们可以直接传入 getter, 此时 computed 是只读了
- 如果我们传入的是一个有 get set 的对象,此时computed 是可写的
- computed 其实只是返回 get set 去生成的一个 ComputedRefImpl 实例
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions,
): ComputedRef<T>
export function computed<T, S = T>(
options: WritableComputedOptions<T, S>,
debugOptions?: DebuggerOptions,
): WritableComputedRef<T, S>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T> | undefined
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack
cRef.onTrigger = debugOptions.onTrigger
}
return cRef as any
}
现在我们仔细来看下 ComputedRefImpl
-
和 RefImpl 一样,每一个 computedRefImp 拥有一个自己的 Dep。在调用 getter 的时候通过 track 追踪当前 activeEffect 和 ref 类似。
-
Computed 如何感知依赖项的变化 ?其实这个能力依赖于 Ref / Reactive 而不是 Comuputed 本身。computed 调用了 ref / reactive 的getter ,自然就被记录到 ref 的或者 reactive 的属性的 dep 上,当他们更新, computed 就会被 ref/reactive 的 trigger 函数后续链路标记为 dirty。
- 注意这里只是标记为 dirty,不会马上更新,而是访问他的 getter 的时候才重新计算,这其实就是 lazy evaluation 。
- computed 相当于 react 的 useMemo,useCallback 可以充当缓存函数使用
-
而 Computed 本身的 dep 记录的是调用 computed 的getter 的内容,比如组件。computed 被 ref / reactive 标记为 dirty 的同时,会调用他的 notify 函数,触发 batch 函数(传入this),batch 通过 this 后续链路相当于调用了 this.dep.trigger,触发依赖的更新。(只不过 batch 有批处理优化)
-
调用他的 getter 的时候会执行 refreshComputed(this),每次都是从新的依赖计算结果。
export class ComputedRefImpl<T = any> implements Subscriber {
/**
* @internal
*/
_value: any = undefined
/**
* @internal
*/
readonly dep: Dep = new Dep(this)
/**
* @internal
*/
readonly __v_isRef = true
// TODO isolatedDeclarations ReactiveFlags.IS_REF
/**
* @internal
*/
readonly __v_isReadonly: boolean
// TODO isolatedDeclarations ReactiveFlags.IS_READONLY
// A computed is also a subscriber that tracks other deps
/**
* @internal
*/
deps?: Link = undefined
/**
* @internal
*/
depsTail?: Link = undefined
/**
* @internal
*/
flags: EffectFlags = EffectFlags.DIRTY
/**
* @internal
*/
globalVersion: number = globalVersion - 1
/**
* @internal
*/
isSSR: boolean
/**
* @internal
*/
next?: Subscriber = undefined
// for backwards compat
effect: this = this
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
/**
* Dev only
* @internal
*/
_warnRecursive?: boolean
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined,
isSSR: boolean,
) {
this[ReactiveFlags.IS_READONLY] = !setter
this.isSSR = isSSR
}
/**
* @internal
*/
notify(): true | void {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
// avoid infinite self recursion
activeSub !== this
) {
batch(this, true)
return true
} else if (__DEV__) {
// TODO warn
}
}
get value(): T {
const link = __DEV__
? this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
: this.dep.track()
refreshComputed(this)
// sync version after evaluation
if (link) {
link.version = this.dep.version
}
return this._value
}
set value(newValue) {
if (this.setter) {
this.setter(newValue)
} else if (__DEV__) {
warn('Write operation failed: computed value is readonly')
}
}
}
Watch
Watch 方法的实现逻辑比较复杂,他还顺带实现了 WatchEffect 等拓展 API。为了啃下这个硬骨头,我们先来看下用法
const stop2 = watch(
count,
(newVal, oldVal, onCleanup) => {
const id = setTimeout(() => {
console.log('异步操作')
}, 1000)
onCleanup(() => {
clearTimeout(id)
console.log('定时器已清除')
})
}
)
// 停止 watch
stop2()
- 传入一个响应式值或者数组作为依赖数组
const stopEffect = watchEffect((onCleanup) => {
const id = setInterval(() => {
console.log('定时器执行')
}, 1000)
onCleanup(() => {
clearInterval(id)
console.log('定时器已清除')
})
})
// 停止 watchEffect,会自动执行清理函数
stopEffect()
- 自动追踪其中的响应式内容
ok,复习好了 watch API 的用法,我们来看下他的核心实现的签名
export function watch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb?: WatchCallback | null,
options: WatchOptions = EMPTY_OBJ,
): WatchHandle
-
如何实现响应式 ?
-
总结来说其实是根据传入的 source 直接计算出依赖的所有属性然后访问他们的,这个执行访问的局部函数的函数叫 getter
-
通过 effect = new ReactiveEffect(getter) ,并执行 effect.run(),让响应式数据收集 ReactiveEffect 为依赖,让我们看下 ReactEffect 做啥。其实他就是响应式数据收集的依赖项,也就是收集的时候 activeLink
-
class ReactiveEffect { constructor(fn, scheduler?) { this.fn = fn this.scheduler = scheduler // ... } run() { // 1. 设置当前激活副作用 // 2. 执行 fn,收集依赖 // 3. 恢复上一个激活副作用 } stop() { // 1. 移除所有依赖 // 2. 标记为已停止 } }
-
-
当响应式数据变更的时候,会触发 effect.shceduler, 在 watch 源码种, effect.scheduler 包含了 job 相关的逻辑
-
effect.scheduler = scheduler ? () => scheduler(job, false) : (job as EffectScheduler)
-
-
job 的逻辑是执行 getter(被挂载在 effect.run() 上) ,比较新旧依赖值是否相同,然后有条件的执行 cb(传入的副作用函数)
-
总结一下触发流程👇
- 响应式数据 set 时,deps 里的 ReactiveEffect 被触发。
- 如果有 scheduler,则调用 scheduler(即 job)。
- job 内部会执行 getter,比较新旧值,然后执行 cb。
现在我们已经大致了解了 watch 的核心逻辑,现在来看下上面提到的 watchEffect 是如何实现的
- 可以看到其实 watchEffect 调用了 doWatch ,doWatch 的签名和 watch 一模一样,其实就是一个收口各种 watchApi 的地方,比如 watchEffect、watchPostEffect、watchSyncEffect,还处理了一下 SSR 环境的操作(笔者没接触过 SSR 就不深入研究了)
- 最终doWatch 中还是调用 baseWatch (也就是上面提到的 watch 函数) 实现响应式逻辑
export function watchEffect(
effect: WatchEffect,
options?: WatchEffectOptions,
): WatchHandle {
return doWatch(effect, null, options)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
options: WatchOptions = EMPTY_OBJ,
): WatchHandle
- 调用 WatchEffect 的时候,其实传入的副作用函数被赋值给个 source ,source 被计算成 getter
- getter 被挂载在 effect.run() 上,执行 effect.scheduler 的时候,先执行 effect.run() ,如果没有传入 cb,说明调用的是 WatchEffect ,如果传入了 cb,比较一下 effect.run() 返回的新值和旧的是否有变化,有变化则执行 cb
画出流程图,再梳理一遍,就清晰多了。