完善响应式能力
约 3273 字大约 11 分钟
2026-03-09
TypeScript 改造
用 TypeScript 来对项目整体进行规范化,把单文件实现拆成
入口、响应式核心(reactive.ts)、依赖系统(effect.ts)三部分修改内容
- 不再使用
buckets来表述依赖容器,而是使用track / trigger这两个函数,来分别处理依赖收集和触发更新的逻辑 - 通过
reactive函数来创建代理对象
reactive.tsimport { track, trigger } from './effect'; export function reactive<T extends object>(target: T): T; export function reactive(target: object) { return new Proxy(target, { get: (target, key, receiver) => { track(target, key); return Reflect.get(target, key, receiver); }, set: (target, key, value, receiver) => { const result = Reflect.set(target, key, value, receiver); trigger(target, key); return result; }, }); }effect.tsexport function track(target: object, key: unknown) {} export function trigger(target: object, key: unknown) {}整个项目使用 Rollup 进行打包
install.shpnpm add -D rollup typescript tslib @types/node @rollup/plugin-commonjs pnpm add -D @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-terser pnpm add -D rollup-plugin-clear rollup-plugin-typescript2 pnpm add -D rollup-plugin-generate-html-templaterollup.config.tsimport { defineConfig } from 'rollup'; export default defineConfig({ input: 'src/index.ts', output: { file: 'dist/index.js', format: 'esm', }, plugins: [ // ... ], });- 不再使用
处理
target在 Vue 中
reactive函数的参数是一个对象,因此需要对传入的target进行类型判断,确保它是一个对象思路
- 封装
isObject工具函数 - 运行时:先用
isObject(target)判断,如果不是对象就直接返回 - 类型层:通过
val is Record<any, any>这个自定义守卫,让 TypeScript 知道后面的target已经可以安全交给Proxy
代码
reactive.tsimport { isObject } from './utils'; export function reactive<T extends object>(target: T): T; export function reactive<T>(target: T): T; export function reactive(target: unknown) { if (!isObject(target)) { return target; } return new Proxy(target, { // ... }); }utils.tsexport const isObject = (val: unknown): val is Record<any, any> => { return typeof val === 'object' && val !== null; };- 封装
处理同一个原始对象被重复代理
这个问题会有两种情况:如果同一个原始对象被重复调用
reactive,或者把一个已经代理过的对象再次传给reactive。而实际上它们应该是同一个响应式对象,不应该被重复代理思路
- 缓存层:用
WeakMap缓存 "原始对象 -> 代理对象" 的映射,如果同一个原始对象已经代理过,就直接返回之前的代理对象 - 标记层:通过额外的标记位
IS_REACTIVE判断传进来的值是不是已经是代理对象,如果已经是代理对象,就直接返回它自己 撒打算
代码
export const enum ReactiveFlags { IS_REACTIVE = '__v_isReactive', } export const reactiveMap = new WeakMap<object, any>(); export function reactive(target: unknown) { if (!isObject(target)) { return target; } // 如果已经有对应的代理对象了,直接返回 if (reactiveMap.has(target)) { return reactiveMap.get(target); } // 如果传入的值已经是代理对象了,直接返回它自己 /** * 因为第二次使用 reactive 时,上次的对象已经变成了代理对象 p1 * 由于是访问一个代理对象的属性,因此触发 get 拦截,通过 key 判断 * 到这个对象已经是一个代理对象了,就直接返回它自己 p1 */ if (target[ReactiveFlags.IS_REACTIVE]) { return target; } const proxy = new Proxy(target, { get(target, key, receiver) { if (key === ReactiveFlags.IS_REACTIVE) { return true; } return Reflect.get(target, key, receiver); }, // ... }); reactiveMap.set(target, proxy); return proxy; }重要
这里用
WeakMap而不用Map,是因为它的 key 只能是对象,并且当原始对象没有被其他地方引用时,可以被垃圾回收,更适合拿来做代理缓存- 缓存层:用
处理
this指向问题如果对象内部存在
getter / setter,且里面用到了this,那么这里的this应该指向代理对象,而不是原始对象。否则内部访问到的属性,就可能绕过代理,拿不到正确的依赖收集和触发更新能力思路
需要使用
Proxy中get和set的最后一个参数receiver,保证this指向代理对象代码
reactive.tsconst proxy = new Proxy(target, { get: (target, key, receiver) => { // ... return Reflect.get(target, key, receiver); }, set: (target, key, value, receiver) => { const result = Reflect.set(target, key, value, receiver); // ... return result; }, });处理深层嵌套对象
如果对象属性的值还是对象,那么只代理最外层是不够的。
Proxy的好处就是可以按需处理,而不是defineProperty那样一次性把所有层级都代理了思路
当
get取出的结果仍然是对象时,需要继续调用reactive(result),把这个返回值也转换成响应式对象代码
reactive.tsconst proxy = new Proxy(target, { get: (target, key, receiver) => { const result = Reflect.get(target, key, receiver); if (isObject(result)) { return reactive(result); } return result; }, });处理
in操作符该操作符用于判断某个属性是否存在,这种读取虽然不是直接取值,但它同样依赖了属性状态,所以也应该进行依赖收集。而
defineProperty它只是对某个属性的get / set而无法在对象结构层面上进行拦截思路
Proxy专门提供了has拦截来处理in操作符,因此只要在has中调用track(target, key),就能把这类访问也纳入响应式系统代码
reactive.tsconst proxy = new Proxy(target, { has: (target, key) => { track(target, key); return Reflect.has(target, key); }, });代码拆分
随着
get、set、has这些拦截逻辑越来越多,并且track / trigger也开始区分不同的操作类型,如果全部继续堆在一个文件里,代码就会越来越难维护思路
把不同职责拆到不同文件里:
reactive.ts负责创建代理对象、处理缓存、处理是否重复代理baseHandlers.ts负责真正的get / set / has拦截实现operations.ts负责统一维护依赖收集和触发更新的操作类型effect.ts负责track / trigger的具体实现
代码
reactive.tsimport { mutableHandlers } from './baseHandlers'; export function reactive(target: unknown) { // ... return new Proxy(target, mutableHandlers); }baseHandlers.tsexport const mutableHandlers: ProxyHandler<object> = { get, set, has, // ... };operations.tsexport const enum TrackOpTypes { GET = 'get', HAS = 'has', ITERATE = 'iterate', } export const enum TriggerOpTypes { SET = 'set', ADD = 'add', DELETE = 'delete', }effect.tsexport function track(target: object, type: TrackOpTypes, key: unknown) { // ... } export function trigger(target: object, type: TriggerOpTypes, key: unknown) { // ... }为什么要拆分不同的操作类型
同样是依赖收集,下面几种读取其实含义不同:
obj.foo:普通取值,对应GET'foo' in obj:判断属性是否存在,对应HASfor...in obj/Object.keys(obj):依赖对象的键集合,对应ITERATE
触发更新也是一样,所以 Vue 才要把操作类型拆细,避免无关更新,让不同依赖能在真正相关的变更下才触发
处理迭代收集
像
for...in、Object.keys()、Object.values()这类操作,本质上依赖的不是某一个具体属性值,而是对象当前 "有哪些 key"。所以又是一个需要在对象结构层面上进行拦截的场景,defineProperty自然又不适合思路
迭代相关操作会触发
Proxy的ownKeys拦截,只需要在ownKeys中调用track(),就能把 "对象键集合" 的依赖单独收集起来代码
baseHandlers.tsexport const ITERATE_KEY = Symbol('iterate'); function ownKeys(target: object): (string | symbol)[] { track(target, TrackOpTypes.ITERATE, ITERATE_KEY); return Reflect.ownKeys(target); } export const mutableHandlers: ProxyHandler<object> = { // ... ownKeys, };operations.tsexport const enum TrackOpTypes { GET = 'get', HAS = 'has', ITERATE = 'iterate', }为什么要引入
ITERATE_KEY这里之所以要引入单独的
ITERATE_KEY,是因为迭代依赖关注的是 "整个键集合",而不是某一个具体属性区分新增和修改操作
在
set拦截里,并不是所有赋值都属于同一种更新。有些是修改已有属性,有些是给对象新增属性,这两种情况后续影响的依赖并不一样,因此需要先区分操作类型思路
- 通过
hasOwnProperty判断当前 key 是否存在于对象上,从而区分新增和修改。如果原本不存在,这次就是ADD;如果原本已经存在,这次就是SET - 对于
SET还要额外比较新旧值是否真的发生变化。如果值没有变,就不应该继续触发更新 - 封装
hasChanged工具函数
代码
baseHandlers.tsfunction set( target: Record<string | symbol, unknown>, key: string | symbol, value: unknown, receiver: object, ): boolean { // 使用 hasOwn 来判断属性是否存在,避免访问原型链上的属性导致误判 const hadKey = Object.prototype.hasOwnProperty.call(target, key); // 保存旧值,后面用来和新值比较是否真的发生变化 const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (!result) { return result; } // 如果 key 不存在,那么就是 ADD if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key); } // 如果 key 已经存在,并且新旧值不相等,那么就是 SET else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key); } return result; }utils.tsexport const hasChanged = (value: unknown, oldValue: unknown): boolean => { return !Object.is(value, oldValue); };- 通过
处理属性移除
删除属性和新增属性一样,都会影响对象的结构。比如某个属性被删掉以后,
in判断结果会变化,迭代结果也会变化,所以这种操作也需要单独触发更新思路
删除操作会触发
Proxy的deleteProperty拦截,除此之外还要先判断目标对象上原本是否真的存在这个属性,再判断删除是否成功:- 原本不存在,就不需要触发更新
- 原本存在且删除成功,才触发
DELETE
代码
baseHandlers.tsfunction deleteProperty( target: Record<string | symbol, unknown>, key: string | symbol, ): boolean { const hadKey = Object.prototype.hasOwnProperty.call(target, key); const result = Reflect.deleteProperty(target, key); // 存在 key 且删除成功,才触发更新 if (hadKey && result) { trigger(target, TriggerOpTypes.DELETE, key); } return result; }operations.tsexport const enum TriggerOpTypes { SET = 'set', ADD = 'add', DELETE = 'delete', }对象标准行为的排除
对象被代理以后,不只是业务代码会读取属性,一些语言层面的标准行为也会触发
get。比如访问__proto__,或者执行Object.prototype.toString.call(proxy)时,内部都会读到一部分内置属性和内置Symbol。这些读取本质上不属于业务依赖。如果也把它们收进响应式系统,就会产生很多无意义的依赖收集,甚至把对象自身的标准行为也误当成响应式数据来处理思路
像
toString这类标准行为,真正读取的往往不是字符串键本身,而是内部的Symbol.toStringTag这类内置Symbol- 通过
getOwnPropertyNames拿到Symbol对象上所有的属性名 - 通过一个变量
builtInSymbols收集这些内置Symbol - 在
get中判断当前key是否属于__proto__或这类内置Symbol,如果属于,就直接返回结果,跳过后面的track和递归代理逻辑
代码
baseHandlers.tsconst builtInSymbols = new Set( Object.getOwnPropertyNames(Symbol) .map((key) => (Symbol as any)[key]) .filter(isSymbol), ); function get(target: object, key: string | symbol, receiver: object): any { const result = Reflect.get(target, key, receiver); if ( isSymbol(key) ? builtInSymbols.has(key as symbol) : key === '__proto__' ) { return result; } track(target, TrackOpTypes.GET, key); if (isObject(result)) { return reactive(result); } return result; }utils.tsexport const isSymbol = (val: unknown): val is symbol => { return typeof val === 'symbol'; };- 通过
现阶段完整代码
现阶段完整代码结构src
index.ts
reactive.ts
baseHandlers.ts
effect.ts
operations.ts
utils.ts
src/index.tsimport { effect } from './effect'; import { reactive } from './reactive'; const obj = reactive({ name: 'John', age: 20, address: { city: 'Beijing', }, }); effect(() => { console.log(obj.name); console.log('age' in obj); console.log(Object.keys(obj)); console.log(obj.address.city); }); obj.name = 'Jane'; delete obj.age;src/reactive.tsimport { mutableHandlers } from './baseHandlers'; import { isObject } from './utils'; export const enum ReactiveFlags { IS_REACTIVE = '__v_isReactive', } export const reactiveMap = new WeakMap<object, any>(); export function reactive<T extends object>(target: T): T; export function reactive<T>(target: T): T; export function reactive(target: unknown) { if (!isObject(target)) { return target; } if (target[ReactiveFlags.IS_REACTIVE]) { return target; } const existingProxy = reactiveMap.get(target); if (existingProxy) { return existingProxy; } const proxy = new Proxy(target, mutableHandlers); reactiveMap.set(target, proxy); return proxy; }src/baseHandlers.tsimport { track, trigger } from './effect'; import { ITERATE_KEY, TrackOpTypes, TriggerOpTypes } from './operations'; import { reactive, ReactiveFlags } from './reactive'; import { hasChanged, hasOwn, isObject, isSymbol } from './utils'; const builtInSymbols = new Set( Object.getOwnPropertyNames(Symbol) .map((key) => (Symbol as any)[key]) .filter(isSymbol), ); function get(target: object, key: string | symbol, receiver: object): any { if (key === ReactiveFlags.IS_REACTIVE) { return true; } const result = Reflect.get(target, key, receiver); if ( isSymbol(key) ? builtInSymbols.has(key as symbol) : key === '__proto__' ) { return result; } track(target, TrackOpTypes.GET, key); if (isObject(result)) { return reactive(result); } return result; } function set( target: Record<string | symbol, unknown>, key: string | symbol, value: unknown, receiver: object, ): boolean { const hadKey = hasOwn(target, key); const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (!result) { return result; } if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key); } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key); } return result; } 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 = hasOwn(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, };src/effect.tsimport { ITERATE_KEY, TriggerOpTypes, TrackOpTypes } from './operations'; type EffectFn = () => void; type KeyToDepMap = Map<unknown, Set<EffectFn>>; const bucket = new WeakMap<object, KeyToDepMap>(); let activeEffect: EffectFn | undefined; export function effect(fn: EffectFn) { const effectFn = () => { activeEffect = effectFn; fn(); activeEffect = undefined; }; effectFn(); return effectFn; } export function track(target: object, type: TrackOpTypes, key: unknown) { if (!activeEffect) { return; } let depsMap = bucket.get(target); if (!depsMap) { depsMap = new Map(); bucket.set(target, depsMap); } let deps = depsMap.get(key); if (!deps) { deps = new Set(); depsMap.set(key, deps); } deps.add(activeEffect); } export function trigger(target: object, type: TriggerOpTypes, key: unknown) { const depsMap = bucket.get(target); if (!depsMap) { return; } const effectsToRun = new Set<EffectFn>(); const effects = depsMap.get(key); effects?.forEach((effectFn) => effectsToRun.add(effectFn)); if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) { const iterateEffects = depsMap.get(ITERATE_KEY); iterateEffects?.forEach((effectFn) => effectsToRun.add(effectFn)); } effectsToRun.forEach((effectFn) => effectFn()); }src/operations.tsexport const enum TrackOpTypes { GET = 'get', HAS = 'has', ITERATE = 'iterate', } export const enum TriggerOpTypes { SET = 'set', ADD = 'add', DELETE = 'delete', } export const ITERATE_KEY = Symbol('iterate');src/utils.tsexport const isObject = (val: unknown): val is Record<any, any> => { return typeof val === 'object' && val !== null; }; export const isSymbol = (val: unknown): val is symbol => { return typeof val === 'symbol'; }; export const hasOwn = ( target: object, key: string | symbol, ): boolean => { return Object.prototype.hasOwnProperty.call(target, key); }; export const hasChanged = (value: unknown, oldValue: unknown): boolean => { return !Object.is(value, oldValue); };
