响应式系统的源码化重构
约 3262 字大约 11 分钟
2026-03-29
为什么这一节要单独做一次重构
到前面为止,响应式系统其实已经具备了这些核心能力:
effect可以完成依赖收集和副作用触发trigger已经能处理调度器、分支清理、数组长度、for...in这些边界问题computed也已经具备了懒执行、缓存和脏值更新的能力
但这些实现更多还是一种 "按功能逐步补齐" 的写法,它的优点是容易理解,缺点是:
- 类型定义还不够清晰
- 依赖结构和副作用函数的关系还没有被统一抽象
trigger、代理层、computed的职责虽然已经有了,但文件边界还不够明确
所以这一节要做的事情,不再是继续增加新能力,而是把前面已经做出来的能力,往 Vue 源码的组织方式上再推进一步
重要
这一节的重点不是新增
Ref和watch,而是先把现有的effect、trigger、computed重构成更接近源码风格的 TS 版本先把副作用系统的类型立起来
这一轮重构最先要处理的,就是
effect周围那些原本靠运行时约定维持的结构在前面的代码里,我们其实一直在操作下面几样东西:
- 当前激活的副作用函数
activeEffect - 保存嵌套调用关系的
effectStack - 存储依赖关系的
WeakMap -> Map -> Set - 挂在副作用函数身上的
deps和options
如果这些东西继续只靠隐式约定来维持,后面就很难继续演进。所以这里先把它们抽成明确的 TS 类型:
ReactiveEffect<T>表示一个可执行、可收集依赖、可挂调度配置的副作用函数Dep表示某个属性对应的副作用函数集合ReactiveEffectOptions用来描述lazy和schedulerKeyToDepMap对应Map<key, Dep>targetMap对应WeakMap<target, KeyToDepMap>
这样一来,原本“函数身上临时挂属性”的做法,就被收进了一个更稳定的类型模型里
提示
这里还有一个顺手做掉的小改动:
buckets会被统一改名成targetMap。因为到了这一步,它已经不再只是一个“桶”,而是完整依赖结构的入口了代码
effect.tsexport interface ReactiveEffect<T = any> { (): T deps: Dep[] options: ReactiveEffectOptions _isEffect: true raw: () => T } export type Dep = Set<ReactiveEffect> export interface ReactiveEffectOptions { lazy?: boolean scheduler?: (job: ReactiveEffect) => void } export type KeyToDepMap = Map<any, Dep> const effectStack: ReactiveEffect[] = [] let activeEffect: ReactiveEffect | undefined const targetMap = new WeakMap<any, KeyToDepMap>()- 当前激活的副作用函数
effect的源码化重构有了类型之后,
effect本身也要继续往前走一步前面我们虽然已经把
effect包装成了effectFn,也解决了分支清理、嵌套执行、调度器这些问题,但它还可以再继续抽象一下:把“创建副作用函数”这件事单独收口到createReactiveEffect里这么做以后,职责会更清晰:
effect负责参数校正和调用入口createReactiveEffect负责真正创建包装后的副作用函数cleanup负责清理旧依赖
同时这里还补了一个边界问题:如果传给
effect的本身就是一个已经包装好的副作用函数,那就直接取它的raw原始函数,避免重复封装代码
effect.tsexport function isEffect(fn: unknown): fn is ReactiveEffect { return !!fn && (fn as ReactiveEffect)._isEffect === true } export function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions = {}, ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (!effectStack.includes(effect)) { cleanup(effect) try { activeEffect = effect effectStack.push(effect) return fn() } finally { effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect<T> effect.options = options effect.deps = [] effect._isEffect = true effect.raw = fn return effect } export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = {}, ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect }trigger和代理层一起做一次结构升级接下来要动的重点就是
trigger前面虽然已经把“谁该触发、谁不该触发”这些情况讲过了,但在源码化重构里,这块会再往前整理成更完整的结构:
trigger先统一用add函数收集要执行的副作用函数- 普通属性更新时,先处理当前
key ADD / DELETE再额外处理ITERATE_KEY- 数组新增元素时,还要额外触发
length - 直接修改数组
length时,再反向影响大于等于新长度的索引依赖
与此同时,
set这边的职责也会跟着收紧:- 先区分这次到底是
ADD还是SET - 再通过
target === toRaw(receiver),确保只对当前代理对象本身触发更新 - 最后把动作类型、新值、旧值统一交给
trigger
这样以后,
baseHandlers.ts和effect.ts的边界就更清楚了:- 代理层负责判断动作类型和触发时机
trigger负责真正决定要运行哪些副作用函数
提示
这一轮重构以后,数组的
length、对象的for...in、普通属性key,都会回到同一个trigger主流程里统一处理,而不是散落在多个地方分别兜底代码
baseHandlers.tsfunction createSetter(shallow = false) { return function set( target: Record<string | symbol, unknown>, key: string | symbol, value: unknown, receiver: object, ): boolean { const oldValue = target[key] const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) 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 } }effect.tsexport function trigger( target: object, type: TriggerOpTypes, key: unknown, newValue?: unknown, oldValue?: unknown, ) { const depsMap = targetMap.get(target) if (!depsMap) { return } const effects = new Set<ReactiveEffect>() const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach((effect) => { if (effect !== activeEffect) { effects.add(effect) } }) } } if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { add(dep) } }) } else { if (key !== void 0) { add(depsMap.get(key)) } switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) } else if (isIntegerKey(key)) { add(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) } break } } effects.forEach((effect) => { if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } }) }computed改成真正独立的模块到这里,
computed也不再适合继续零散地放在测试代码里了,而是应该单独提成一个computed.ts这一轮改造的重点有两块:
- 给
computed本身补上参数类型和返回值类型 - 把内部状态统一收进
ComputedRefImpl类里
参数层面,
computed需要同时支持两种调用方式:- 直接传
getter - 传
{ get, set }对象
返回值层面,这里先只关心
computed自己的结果类型,不把Ref和watch的实现提前并进来。所以这一节只声明ComputedRef和WritableComputedRef,不展开后面的Ref主线另外还有一个和源码更接近的点:在 TS 环境里,
ComputedRefImpl可以直接通过ReactiveFlags.IS_READONLY区分当前计算属性是不是只读重要
这一节虽然已经出现了只读计算属性和可写计算属性的区分,但这里仍然只是在完善
computed自身,不等于已经开始讲Ref代码
computed.tsexport type ComputedGetter<T> = (ctx?: any) => T export type ComputedSetter<T> = (value: T) => void export interface WritableComputedOptions<T> { get: ComputedGetter<T> set: ComputedSetter<T> } export interface WritableComputedRef<T> { readonly effect: ReactiveEffect<T> value: T } export interface ComputedRef<T = any> extends WritableComputedRef<T> { readonly value: T } export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T> export function computed<T>( options: WritableComputedOptions<T>, ): WritableComputedRef<T> export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>, ) { let getter: ComputedGetter<T> let setter: ComputedSetter<T> if (isFunction(getterOrOptions)) { getter = getterOrOptions setter = NOOP as ComputedSetter<T> } else { getter = getterOrOptions.get setter = getterOrOptions.set } return new ComputedRefImpl( getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set, ) }computed.ts(类实现)class ComputedRefImpl<T> { private _value!: T private _dirty = true public readonly effect: ReactiveEffect<T> public readonly [ReactiveFlags.IS_READONLY]: boolean constructor( getter: ComputedGetter<T>, private readonly _setter: ComputedSetter<T>, isReadonly: boolean, ) { this.effect = effect(getter, { lazy: true, scheduler: () => { if (!this._dirty) { this._dirty = true trigger(toRaw(this), TriggerOpTypes.SET, 'value') } }, }) this[ReactiveFlags.IS_READONLY] = isReadonly } get value() { if (this._dirty) { this._value = this.effect() this._dirty = false } track(toRaw(this), TrackOpTypes.GET, 'value') return this._value } set value(newValue: T) { this._setter(newValue) } }- 给
最终完整代码
下面这份代码树,只保留这三节真正涉及到的几个文件:
effecttriggercomputed- 它们依赖到的
reactive / baseHandlers / utils / operations
注
这里先不把
Ref和watch合进来,等后面真正写到这两节的时候,再继续往这个结构上补响应式系统重构后的核心结构src
operations.ts
utils.ts
reactive.ts
effect.ts
baseHandlers.ts
computed.ts
src/operations.tsexport const enum TrackOpTypes { GET = 'GET', HAS = 'HAS', ITERATE = 'ITERATE', } export const enum TriggerOpTypes { SET = 'SET', ADD = 'ADD', DELETE = 'DELETE', }src/utils.tsexport const isObject = (val: unknown): val is Record<any, any> => { return val !== null && typeof val === 'object' } export const isString = (val: unknown): val is string => { return typeof val === 'string' } export const isFunction = (val: unknown): val is Function => { return typeof val === 'function' } export const isSymbol = (val: unknown): val is symbol => { return typeof val === 'symbol' } export const isArray = Array.isArray export const extend = Object.assign export const NOOP = () => {} export const hasChanged = (value: unknown, oldValue: unknown): boolean => { return !Object.is(value, oldValue) } export const isIntegerKey = (key: unknown) => { return ( isString(key) && key !== 'NaN' && key[0] !== '-' && '' + parseInt(key, 10) === key ) } const hasOwnProperty = Object.prototype.hasOwnProperty export const hasOwn = ( val: object, key: string | symbol, ): key is keyof typeof val => hasOwnProperty.call(val, key)src/reactive.tsimport { mutableHandlers, readonlyHandlers, shallowReactiveHandlers } from './baseHandlers' import { isObject } from './utils' export const enum ReactiveFlags { IS_REACTIVE = '__v_isReactive', IS_READONLY = '__v_isReadonly', RAW = '__v_raw', SKIP = '__v_skip', } export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any } export const reactiveMap = new WeakMap<Target, any>() export const readonlyMap = new WeakMap<Target, any>() function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, ) { if (!isObject(target)) { return target } const proxyMap = isReadonly ? readonlyMap : reactiveMap const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } if (target[ReactiveFlags.RAW] && target[ReactiveFlags.IS_REACTIVE]) { return target } const proxy = new Proxy(target, baseHandlers) proxyMap.set(target, proxy) return proxy } export function reactive<T extends object>(target: T): T export function reactive(target: object) { if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } return createReactiveObject(target, false, mutableHandlers) } type DeepReadonly<T extends Record<string, any>> = T extends any ? { readonly [K in keyof T]: T[K] extends Record<string, any> ? DeepReadonly<T[K]> : T[K] } : never export function readonly<T extends object>(target: T): DeepReadonly<T> { return createReactiveObject(target, true, readonlyHandlers) } export function shallowReactive<T extends object>(target: T): T { return createReactiveObject(target, false, shallowReactiveHandlers) } export function toRaw<T>(observed: T): T { return (observed as Target)[ReactiveFlags.RAW] || observed }src/effect.tsimport { TrackOpTypes, TriggerOpTypes } from './operations' import { isArray, isIntegerKey } from './utils' export interface ReactiveEffect<T = any> { (): T deps: Dep[] options: ReactiveEffectOptions _isEffect: true raw: () => T } export type Dep = Set<ReactiveEffect> export interface ReactiveEffectOptions { lazy?: boolean scheduler?: (job: ReactiveEffect) => void } export type KeyToDepMap = Map<any, Dep> export const ITERATE_KEY = Symbol('') let activeEffect: ReactiveEffect | undefined const effectStack: ReactiveEffect[] = [] const targetMap = new WeakMap<any, KeyToDepMap>() let shouldTrack = true export function isEffect(fn: unknown): fn is ReactiveEffect { return !!fn && (fn as ReactiveEffect)._isEffect === true } export function pauseTracking() { shouldTrack = false } export function enableTracking() { shouldTrack = true } export function track(target: object, type: TrackOpTypes, key: unknown) { if (!shouldTrack || activeEffect === undefined) { return } let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let deps = depsMap.get(key) if (!deps) { depsMap.set(key, (deps = new Set())) } if (!deps.has(activeEffect)) { deps.add(activeEffect) activeEffect.deps.push(deps) } } export function trigger( target: object, type: TriggerOpTypes, key: unknown, newValue?: unknown, oldValue?: unknown, ) { const depsMap = targetMap.get(target) if (!depsMap) { return } const effects = new Set<ReactiveEffect>() const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach((effect) => { if (effect !== activeEffect) { effects.add(effect) } }) } } if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { add(dep) } }) } else { if (key !== void 0) { add(depsMap.get(key)) } switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) } else if (isIntegerKey(key)) { add(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) } break } } effects.forEach((effect) => { if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } }) } export function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions = {}, ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (!effectStack.includes(effect)) { cleanup(effect) try { activeEffect = effect effectStack.push(effect) return fn() } finally { effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect<T> effect.options = options effect.deps = [] effect._isEffect = true effect.raw = fn return effect } export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = {}, ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect } 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 } }src/baseHandlers.tsimport { trigger, track, pauseTracking, enableTracking, ITERATE_KEY, } from './effect' import { isObject, hasChanged, isArray, isSymbol, extend, isIntegerKey, hasOwn, } from './utils' import { ReactiveFlags, reactive, reactiveMap, readonlyMap, toRaw, readonly, } from './reactive' import { TrackOpTypes, TriggerOpTypes } from './operations' const builtInSymbols = new Set( Object.getOwnPropertyNames(Symbol) .map((key) => (Symbol as any)[key]) .filter(isSymbol), ) const arrayInstrumentations: Record<string, Function> = {} ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach((key) => { 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) { return method.apply(arr, args.map(toRaw)) } 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) enableTracking() return res } }) function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object): any { if (key === ReactiveFlags.IS_REACTIVE) { return true } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? readonlyMap : reactiveMap).get(target) ) { return target } const targetIsArray = isArray(target) if (targetIsArray && Object.prototype.hasOwnProperty.call(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } const result = Reflect.get(target, key, receiver) const keyIsSymbol = isSymbol(key) if (keyIsSymbol ? builtInSymbols.has(key as symbol) : key === '__proto__') { return result } if (!isReadonly) { track(target, TrackOpTypes.GET, key) } if (shallow) { return result } if (isObject(result)) { return isReadonly ? readonly(result) : reactive(result) } return result } } function createSetter(shallow = false) { return function set( target: Record<string | symbol, unknown>, key: string | symbol, value: unknown, receiver: object, ): boolean { const oldValue = target[key] const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) 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 } } const get = createGetter() const readonlyGet = createGetter(true) const shallowGet = createGetter(false, true) const set = createSetter() const shallowSet = createSetter(true) function has(target: object, key: string | symbol): boolean { track(target, TrackOpTypes.HAS, key) return Reflect.has(target, key) } function ownKeys(target: object): (string | symbol)[] { track(target, TrackOpTypes.ITERATE, ITERATE_KEY) return Reflect.ownKeys(target) } function deleteProperty( target: Record<string | symbol, unknown>, key: string | symbol, ): boolean { const hadKey = Object.prototype.hasOwnProperty.call(target, key) const result = Reflect.deleteProperty(target, key) if (hadKey && result) { trigger(target, TriggerOpTypes.DELETE, key) } return result } export const mutableHandlers: ProxyHandler<object> = { get, set, has, ownKeys, deleteProperty, } export const readonlyHandlers: ProxyHandler<object> = { get: readonlyGet, set(target, key) { console.warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target, ) return true }, deleteProperty(target, key) { console.warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target, ) return true }, } export const shallowReactiveHandlers: ProxyHandler<object> = extend( {}, mutableHandlers, { get: shallowGet, set: shallowSet, }, )src/computed.tsimport type { ReactiveEffect } from './effect' import { effect, track, trigger } from './effect' import { TrackOpTypes, TriggerOpTypes } from './operations' import { ReactiveFlags, toRaw } from './reactive' import { isFunction, NOOP } from './utils' export type ComputedGetter<T> = (ctx?: any) => T export type ComputedSetter<T> = (value: T) => void export interface WritableComputedOptions<T> { get: ComputedGetter<T> set: ComputedSetter<T> } export interface WritableComputedRef<T> { readonly effect: ReactiveEffect<T> value: T } export interface ComputedRef<T = any> extends WritableComputedRef<T> { readonly value: T } export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T> export function computed<T>( options: WritableComputedOptions<T>, ): WritableComputedRef<T> export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>, ) { let getter: ComputedGetter<T> let setter: ComputedSetter<T> if (isFunction(getterOrOptions)) { getter = getterOrOptions setter = NOOP as ComputedSetter<T> } else { getter = getterOrOptions.get setter = getterOrOptions.set } return new ComputedRefImpl( getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set, ) } class ComputedRefImpl<T> { private _value!: T private _dirty = true public readonly effect: ReactiveEffect<T> public readonly [ReactiveFlags.IS_READONLY]: boolean constructor( getter: ComputedGetter<T>, private readonly _setter: ComputedSetter<T>, isReadonly: boolean, ) { this.effect = effect(getter, { lazy: true, scheduler: () => { if (!this._dirty) { this._dirty = true trigger(toRaw(this), TriggerOpTypes.SET, 'value') } }, }) this[ReactiveFlags.IS_READONLY] = isReadonly } get value() { if (this._dirty) { this._value = this.effect() this._dirty = false } track(toRaw(this), TrackOpTypes.GET, 'value') return this._value } set value(newValue: T) { this._setter(newValue) } }
