简述
事实上,响应式并不是 Vue3
的新概念,它是一个核心概念。在实际代码中,响应式系统经常与组件化相提并论。更进一步的说,组件化赋予了开发者模块化代码的能力,而响应式系统则让开发者可以通过数据控制组件的呈现方式。
不过这东西从本质上来讲,其实就是劫持数据的变化,在数据变化后自动的执行一些副作用函数。
如果你大致的了解过这个东西,你大概会知道,Vue2
和 Vue3
的响应式系统实现有些略微不同。
从表象上来说,Vue2
的响应式系统是黑盒。Vue2
承包了一切工作,你只需要将数据定义在诸如 data
、props
、computed
等选项中就行。而 Vue3
则是把这个决定权交给了开发者。由开发者来决定究竟哪些数据应该是响应式的。
从实现上来说,Vue2
使用 Object.defineproperty
来实现数据劫持,而 Vue3
则使用了 proxy
。
从逻辑上讲,其实二者是相同的,都是为了实现核心的数据劫持。
不过 Vue
毕竟是一个投入到实际生产中使用的框架,仅仅完成理论上的实现当然是不行的。从 Object.defineproperty
到 proxy
的切换实际上也表现出了一些技术上的选择。
简单来说,因为 Vue2
对响应式数据黑盒化的设计,在框架初始化的时候,会递归遍历所有数据,然后使用 Object.defineproperty
来做劫持。这是一个解决方案,但并不够好。因为首先会付出很多性能消耗,其次,并不是每一条数据都需要变成响应式的。递归消耗的性能支出是否合算,全看开发者的具体实现方式。另外,Object.defineproperty
也不能监听到对象属性的新增与删除。
所以 Vue3
使用了 proxy
和显式的 reactice API
。这样不仅可以让开发者自行决定哪些数据需要变成响应式的,还能减少劫持时的数据消耗。
除此之外,在具体实现中,Vue3
也做了一些调整来优化性能。更具体的东西,来直接看源码吧。
入口 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export function reactive <T extends object >(target: T ): UnwrapNestedRefs <T > ;export function reactive (target: object ) { if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target; } return creativeReactiveObject( target, false , mutableHandlers, mutableCollectionHandlers ); }
简单来说,这个入口函数主要是处理一下只读的 proxy
,直接返回。如果不是只读的,那么就进入 creativeReactiveObject
函数的创建逻辑。
这样做的好处就是封装,一个函数只专注于一件事情。让创建响应式对象的函数去关心传入的对象是不是只读的,属实有点职责不清。
creativeReactiveObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export const reactiveMap = new WeakMap<Target, any >();export const readonlyMap = new WeakMap<Target, any >();function createReactiveObject ( target: Target, isReadonly: boolean , baseHandlers: ProxyHandler<any >, collectionHandlers: ProxyHandler<any > ) { const proxyMap = isReadonly ? readonlyMap : reactiveMap; const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ); proxyMap.set(target, proxy); return proxy; }
简单来说,这个创建过程只有两步:
使用 Proxy 做好劫持
创建 WeakMap 映射
mutableHandlers
根据数据是否是 collection
类型的,划分出了两种劫持实现。因为我们关注的是响应式系统的原理,所以就选择 mutableHandlers
来研究。如果你有兴趣的话,可以自己读一下 mutableCollectionHandlers
的代码。
1 2 3 4 5 6 7 export const mutableHandlers: ProxyHandler<object> = { get , set , deleteProperty, has, ownKeys, };
代码一目了然,就是劫持了这些操作。
接下来,就是在 get
中进行依赖收集,然后在 set
中派发通知了。我们分别说说这俩。
get 到 track 的依赖收集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const get = createGetter();function createGetter (isReadonly = false , shallow = false ) { return function get (target: Target, key: string | symbol, receiver: object ) { const res = Reflect.get(target, key, receiver); if (!isReadonly) { track(target, TrackOpTypes.GET, key); } return res; }; }
既然是 getter
函数,那么求值就是必要的。这里使用了 Reflect
来求值。
接着,如果不是只读的,那么就调用 track
函数来收集依赖。下面是 track
函数的实现
track
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const targetMap = new WeakMap<any , KeyToDepMap>();export function track (target: object, type : TrackOpTypes, key: unknown ) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); activeEffect.deps.push(dep); } }
这是一个嵌套的映射结构。代码很清晰,不过如果用图展示映射关系的话,会更清晰:
根据代码,我们放入 depsMap
中的是一些 activeEffect
。这就是所谓的副作用函数。换句话说,就是数据变化时,需要自动执行的那些函数。
你现在应该已经清楚了,依赖收集到底是什么?依赖收集其实就是搞了一堆映射关系,记录一下哪些数据有哪些副作用函数。不过你可能对依赖这东西到底是啥还有点迷惑。那么我们就来看看怎么设定一个依赖函数。
effect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const effectStack: ReactiveEffect[] = [];let activeEffect: ReactiveEffect | undefined ;export function effect <T = any >( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect <T > { const effect = createReactiveEffect(fn, options); return effect; } function createReactiveEffect <T = any >( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect <T > { const effect = function reactiveEffect ( ): unknown { activeEffect = effect; return fn(); } as ReactiveEffect; return effect; }
为了说明最本质的东西,我改写了一下 createReactiveEffect
函数。其实这个函数就做了两件事,将 effect
指向全局的 activeEffect
,然后执行绑定的 fn
函数。
另外的,你应该已经注意到了,这个 effect
实际上是个函数,是个待执行的函数。那么它将在什么时候执行呢?答案就是 set
派发通知的时候。
真正的 createReactiveEffect
不过我们先不看派发通知。刚才为了说明逻辑,大幅简化了 createReactiveEffect
函数的逻辑,不过如果你在看源码,你会发现这里的实现比较复杂。
简单来说,就是使用了 栈
来执行依赖函数,这样做的原因是为了处理嵌套 effect
。我们直接来看完整版的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const effectStack: ReactiveEffect[] = [];let activeEffect: ReactiveEffect | undefined ;function createReactiveEffect <T = any >( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect <T > { const effect = function reactiveEffect ( ): unknown { if (!effect.active) { return options.scheduler ? undefined : fn(); } if (!effectStack.includes(effect)) { cleanup(effect); try { enableTracking(); effectStack.push(effect); activeEffect = effect; return fn(); } finally { effectStack.pop(); resetTracking(); activeEffect = effectStack[effectStack.length - 1 ]; } } } as ReactiveEffect; effect.id = uid++; return effect; }
处理嵌套 effect
你应该注意到了这两句代码:
1 2 3 effectStack.push(effect); effectStack.pop();
这是个典型的入栈出栈操作。
这个设计是为了解决嵌套的 effect
而实现的。我们按照嵌套方式,将函数压入栈,然后从栈顶的函数开始执行,将 activeEffect
指向这个函数。然后执行完一个弹出一个,再执行外层的 effect
函数。
也许你会觉得这个设计很熟悉,对了,这个其实就是函数的执行栈在处理闭包时候的操作。
cleanup
另外的,你应该注意到了在实际处理依赖函数前,我们调用了 cleanup
函数。下面是它的实现:
1 2 3 4 5 6 7 8 9 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
的映射。下面解释一下这样做的理由。
设 data
集合,是页面代码所有数据的集合。renderedData
集合,是渲染在了页面上的数据的集合。postRenderedData
是页面变化后渲染在页面上的数据的集合。
那么,有以下几个结论:
renderedData
是 data
的子集。
postRenderedData
是 data
的子集。
postRenderedData
与 renderedData
不一定相等。
这几个结论不难理解。页面在变化的过程中,并不是所有的数据都会呈现在页面上。但是在第一次呈现的时候,会触发 get
函数,进而进行依赖收集。可是在页面变化后,可能有一部分已经被收集依赖的数据并不会呈现在页面上。
如果我们不做任何处理,那么一旦那些未被渲染的数据改变,也会触发整体页面的重新渲染。这种逻辑显然是错误的,页面怎么能因为一个没有被展现在页面上的数据而重新渲染呢?
所以,这就是 cleanup
函数的理由。清除这些依赖。防止预期之外的重新渲染。
set 到 trigger 的派发通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const set = createSetter();function createSetter (shallow = flase ) { return function set ( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { 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; }; }
简单来说,分为两步:
使用 Reflect.set
设定了值
调用 trigger
来派发通知
在具体调用 trigger
时判断了一下原型。如果 target
的原型是一个 proxy
,那么使用 Reflect.set
设置属性的时候会再次触发 setter
。所以这里需要优化。
不过这部分对于核心流程的理解来说无足轻重。
trigger
下面是 trigger
函数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 export function trigger ( target: object, type : TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { const depsMap = targetMap.get(target); const effects = new Set<RectiveEffect>(); const add = (effectsToAdd: Set<ReactiveEffect> | undefined ) => { }; const run = (effect: ReactiveEffect ) => { if (effect.options.scheduler) { effect.options.scheduler(effect); } else { effect(); } }; effects.forEach(run); }
这个函数的实际源码很长,不过做的事情很简单,就两件事:
将 depsMap 中的 effect 函数,根据所做事情的 key,添加到 effects 集合里
遍历 effects 集合,执行副作用函数
渲染副作用函数
过在上一章结尾,我们提到了一句渲染流程和响应式系统结合的部分,就是这句代码:
1 2 3 4 5 6 7 8 9 10 instance.update = effect( function componentEffect ( ) { if (!instance.isMounted) { } else { } }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions );
注意到了吗,这里其实就是注册了副作用函数,然后将这东西绑定在了 instance.update
上。这就是为什么在你更改一个响应式数据后,页面会自动刷新了。
补充
如果你也去看一下 watch
、computed
功能的实现,你会发现其实它们也是通过 effect
来注册副作用函数的。例如实现 computed
的这段源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function doWatch ( source: WatchSource | WatchSource[] | WatchEffect, cb: WatchCallback || null , { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ, instance = currentInstance ): WatchStopHandle { const runner = effect(getter, { lazy: true , onTrack, onTrigger, scheduler }) return () => { stop(runner) if (instance) { remove(instance.effects!, runner) } } }
不过这些实际 api
的实现留到以后再说。
总结
到这里,响应式系统的核心逻辑就已经介绍完了。大致总结一下:
响应式系统使用 proxy
来实现数据劫持
数据劫持中 getter
调用 track
函数收集依赖
数据劫持中 setter
调用 trigger
函数派发通知
另外的,响应式系统还提供了一些其他的常用 api,例如 readonly
、ref
等,你可以自己去查看源码。
最后,你还可以参考 官方的响应式原理教程 ,这是学习的最好资料。