Vue3的数据响应式系统主要由@vue/reactivity承担,主要包括副作用函数effect,依赖收集track和触发更新trigger这三部分。
Composition API实现响应式的关键API为包装基本类型的ref和引用类型的reactive。
仅对简单的对象的reactive过程做考虑的话,代码如下:
1const proxyMap = new WeakMap<Target, any>()
2
3export function reactive(target: object) {
4 const proxy = new Proxy(target, handlers)
5
6 proxyMap.set(target, proxy)
7
8 return proxy
9}逻辑很简单,就是通过Proxy返回目标的对象的代理,并将目标的对象和代理的映射关系缓存到一个proxyMap中,其代理的handlers内容如下:
1const handlers = {
2 get(target: Target, key: string | symbol, receiver: object){
3 const res = Reflect.get(target, key, receiver)
4
5 track(target, TrackOpTypes.GET, key)
6
7 return res
8 },
9 set(target: object, key: string | symbol, value: unknown, receiver: object): boolean{
10 const oldValue = (target as any)[key]
11 const hadKey = hasOwn(target, key)
12 const result = Reflect.set(target, key, value, receiver)
13
14 if (target === toRaw(receiver)) {
15 if (!hadKey) {
16 trigger(target, TriggerOpTypes.ADD, key, value)
17 } else if (hasChanged(value, oldValue)) {
18 trigger(target, TriggerOpTypes.SET, key, value)
19 }
20 }
21
22 return result
23 }
24}我们先只关注基本的get和set ProxyHandler,其逻辑也比较简单。其中get ProxyHandler除了调用默认的Reflect API,就是调用了track方法。类似的,set ProxyHandler除了调用默认的Reflect API以外,根据目标对象target中是否有要set的key,没有时调用了TriggerOpTypes.ADD类型的trigger方法,已经有key,并且值发生了改变时,调用了TriggerOpTypes.SET类型的trigger方法。
Vue3中的依赖以副作用函数effect的方式体现。副作用函数就是指会产生副作用的函数,就是该函数的执行会直接或者间接影响其他的函数的执行,例如更新DOM,修改作用域以外的变量等。
track和trigger主要负责依赖的收集(追踪)和触发,类似于Vue2中的Dep,分别在响应式数据get和set操作中被执行。用于将响应式数据和effect关联起来。
这两个方法位于@vue/reactivity/src/effect.ts文件中,在解析这两个方法之前,需要先对这两个方法依赖的几个局部变量做下说明:
1type Dep = Set<ReactiveEffect>
2type KeyToDepMap = Map<any, Dep>
3
4const targetMap = new WeakMap<any, KeyToDepMap>()
5let activeEffect: ReactiveEffect | undefined
6let shouldTrack = true
7const trackStack: boolean[] = []targetMap是一个WeakMap,其key目标对象target,其value是一个以目标对象target的key为key、其依赖的集合为value的Map,所以targetMap是一个所有目标对象target的依赖集合的映射总集合。
activeEffect为当前激活状态的effect,具体在effect分析
shouldTrack用于表示是否需要开始依赖收集,trackStack用于标识当前依赖收集的深度。主要用于effect方法中。
精简后代码如下:
1export function track(target: Target, type: TrackOpTypes, key: unknown) {
2 if (!shouldTrack || activeEffect === undefined) {
3 return
4 }
5 let depsMap = targetMap.get(target)
6 if (!depsMap) {
7 targetMap.set(target, (depsMap = new Map()))
8 }
9 let dep = depsMap.get(key)
10 if (!dep) {
11 depsMap.set(key, (dep = new Set()))
12 }
13 if (!dep.has(activeEffect)) {
14 dep.add(activeEffect)
15 activeEffect.deps.push(dep)
16 }
17}从上面代码可以知道,track的逻辑非常简单,就是向targetMap的具体dep中加入当前activeEffect,并像activeEffect的deps中加入对于的dep。
1export function trigger(
2 target: object,
3 type: TriggerOpTypes,
4 key?: unknown,
5 newValue?: unknown
6) {
7 const depsMap = targetMap.get(target)
8 if (!depsMap) {
9 // never been tracked
10 return
11 }
12
13 // 需要触发的effect集合,通过add向其中添加内容
14 const effects = new Set<ReactiveEffect>()
15 const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
16 if (effectsToAdd) {
17 effectsToAdd.forEach(effect => {
18 if (effect !== activeEffect || effect.allowRecurse) {
19 effects.add(effect)
20 }
21 })
22 }
23 }
24
25 // 不同的情况下,出发depsMap里的不同内容
26 if (type === TriggerOpTypes.CLEAR) {
27 depsMap.forEach(add)
28 } else if (key === 'length' && isArray(target)) {
29 depsMap.forEach((dep, key) => {
30 if (key === 'length' || key >= (newValue as number)) {
31 add(dep)
32 }
33 })
34 } else {
35 if (key !== void 0) {
36 add(depsMap.get(key))
37 }
38
39 switch (type) {
40 case TriggerOpTypes.ADD:
41 if (!isArray(target)) {
42 add(depsMap.get(ITERATE_KEY))
43 if (isMap(target)) {
44 add(depsMap.get(MAP_KEY_ITERATE_KEY))
45 }
46 } else if (isIntegerKey(key)) {
47 add(depsMap.get('length'))
48 }
49 break
50 case TriggerOpTypes.DELETE:
51 if (!isArray(target)) {
52 add(depsMap.get(ITERATE_KEY))
53 if (isMap(target)) {
54 add(depsMap.get(MAP_KEY_ITERATE_KEY))
55 }
56 }
57 break
58 case TriggerOpTypes.SET:
59 if (isMap(target)) {
60 add(depsMap.get(ITERATE_KEY))
61 }
62 break
63 }
64 }
65
66 // 触发effect
67 const run = (effect: ReactiveEffect) => {
68 // 异步更新队列
69 if (effect.options.scheduler) {
70 effect.options.scheduler(effect)
71 } else { // 立即更新
72 effect()
73 }
74 }
75
76 effects.forEach(run)
77}trigger的主要逻辑为在targetMap中去除对应的depsMap,然后根据不同的数据类型,不同的TriggerOpTypes,触发对应的effect。更新方式有异步更新队列和立即更新两种。
effect方法返回ReactiveEffect函数,作用相当于vue2中的观察者Watcher。简化后代码如下:
1export function pauseTracking() {
2 trackStack.push(shouldTrack)
3 shouldTrack = false
4}
5
6export function enableTracking() {
7 trackStack.push(shouldTrack)
8 shouldTrack = true
9}
10
11export function resetTracking() {
12 const last = trackStack.pop()
13 shouldTrack = last === undefined ? true : last
14}
15
16const effectStack: ReactiveEffect[] = []
17let uid = 0
18
19function createReactiveEffect<T = any>(
20 fn: () => T,
21 options: ReactiveEffectOptions
22): ReactiveEffect<T> {
23 const effect = function reactiveEffect(): unknown {
24 if (!effect.active) {
25 return options.scheduler ? undefined : fn()
26 }
27 if (!effectStack.includes(effect)) {
28 cleanup(effect)
29 try {
30 enableTracking()
31 effectStack.push(effect)
32 activeEffect = effect
33 return fn()
34 } finally {
35 effectStack.pop()
36 resetTracking()
37 activeEffect = effectStack[effectStack.length - 1]
38 }
39 }
40 } as ReactiveEffect
41 effect.id = uid++
42 effect.allowRecurse = !!options.allowRecurse
43 effect._isEffect = true
44 effect.active = true
45 effect.raw = fn
46 effect.deps = [] // 与该副作用函数存在联系的依赖的集合,在该effect执行时,会先将对应的deps清空,即cleanup函数的作用,以便effect执行后,在track过程中重新建立联系,从而避免副作用函数产生遗留
47 effect.options = options
48 return effect
49}
50
51function cleanup(effect: ReactiveEffect) {
52 const { deps } = effect
53 if (deps.length) {
54 for (let i = 0; i < deps.length; i++) {
55 deps[i].delete(effect)
56 }
57 deps.length = 0
58 }
59}
60
61export function isEffect(fn: any): fn is ReactiveEffect {
62 return fn && fn._isEffect === true
63}
64
65export function effect<T = any>(
66 fn: () => T,
67 options: ReactiveEffectOptions = EMPTY_OBJ
68): ReactiveEffect<T> {
69 if (isEffect(fn)) {
70 fn = fn.raw
71 }
72 const effect = createReactiveEffect(fn, options)
73 if (!options.lazy) {
74 effect()
75 }
76 return effect
77}reactive方法源码位于@vue/reactivity/src/reactive.ts的第63行,其逻辑很简单,先判断是否为readonly,如果是,直接返回target,否则调用createReactiveObject方法:
1export function reactive(target: object) {
2 // if trying to observe a readonly proxy, return the readonly version.
3 if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
4 return target
5 }
6 return createReactiveObject(
7 target,
8 false,
9 mutableHandlers,
10 mutableCollectionHandlers
11 )
12}createReactiveObject方法位于同文件的第136行:
1function createReactiveObject(
2 target: Target,
3 isReadonly: boolean,
4 baseHandlers: ProxyHandler<any>,
5 collectionHandlers: ProxyHandler<any>
6) {
7 // 非对象,直接返回
8 if (!isObject(target)) return target
9
10 // 已经是一个Reactive Proxy,但不是Reactive Readonly Proxy直接返回
11 if (
12 target[ReactiveFlags.RAW] &&
13 !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
14 ) return target
15
16 // 已经在proxyMap中有对应的Reactive Proxy,直接返回existingProxy
17 const proxyMap = isReadonly ? readonlyMap : reactiveMap
18 const existingProxy = proxyMap.get(target)
19 if (existingProxy) {
20 return existingProxy
21 }
22
23 // 不是禁止Reactive的类型
24 const targetType = getTargetType(target)
25
26 if (targetType === TargetType.INVALID) {
27 return target
28 }
29
30 // 创建Reactive Proxy
31 const proxy = new Proxy(
32 target,
33 targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
34 )
35
36 // 在proxyMap中设置缓存
37 proxyMap.set(target, proxy)
38
39 return proxy
40}在new Proxy,根据TargetType分别使用了collectionHandlers和baseHandlers,先看下baseHandlers其值由reactive调用时传入的实参mutableHandlers,其定义位于@vue/reactivity/src/baseHandlers.ts的第187行:
1export const mutableHandlers: ProxyHandler<object> = {
2 get,
3 set,
4 deleteProperty,
5 has,
6 ownKeys
7}接下来我们一次看下对应的ProxyHandler:
mutableHandlers的get``ProxyHandler定义位于同文件的第35行,调用了同文件的第72行的createGetter。
1const get = /*#__PURE__*/ createGetter()
2
3function createGetter(isReadonly = false, shallow = false) {
4 return function get(target: Target, key: string | symbol, receiver: object) {
5 if (key === ReactiveFlags.IS_REACTIVE) {
6 return !isReadonly
7 } else if (key === ReactiveFlags.IS_READONLY) {
8 return isReadonly
9 } else if (
10 key === ReactiveFlags.RAW &&
11 receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
12 ) {
13 return target
14 }
15
16 const targetIsArray = isArray(target)
17 if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
18 return Reflect.get(arrayInstrumentations, key, receiver)
19 }
20
21 const res = Reflect.get(target, key, receiver)
22
23 const keyIsSymbol = isSymbol(key)
24 if (
25 keyIsSymbol
26 ? builtInSymbols.has(key as symbol)
27 : key === `__proto__` || key === `__v_isRef`
28 ) {
29 return res
30 }
31
32 if (!isReadonly) {
33 track(target, TrackOpTypes.GET, key)
34 }
35
36 if (shallow) {
37 return res
38 }
39
40 if (isRef(res)) {
41 // ref unwrapping - does not apply for Array + integer key.
42 const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
43 return shouldUnwrap ? res.value : res
44 }
45
46 if (isObject(res)) {
47 // Convert returned value into a proxy as well. we do the isObject check
48 // here to avoid invalid value warning. Also need to lazy access readonly
49 // and reactive here to avoid circular dependency.
50 return isReadonly ? readonly(res) : reactive(res)
51 }
52
53 return res
54 }
55}其主要逻辑为调用@vue/reactivity/src/effect.ts的第141行的track方法,并且根据类型对值进行如下的特殊处理:将ref执行unwrap,引用类型递归转为proxy。
track方法主要作用是追踪响应,将需要被追踪的对象作为键更新到全局的depsMap里,并与activeEffect关联起来,相当于Vue2中的Dep。
1const targetMap = new WeakMap<any, KeyToDepMap>()
2let activeEffect: ReactiveEffect | undefined
3let shouldTrack = true
4
5export function track(target: object, type: TrackOpTypes, key: unknown) {
6 if (!shouldTrack || activeEffect === undefined) {
7 return
8 }
9 let depsMap = targetMap.get(target)
10 if (!depsMap) {
11 targetMap.set(target, (depsMap = new Map()))
12 }
13 let dep = depsMap.get(key)
14 if (!dep) {
15 depsMap.set(key, (dep = new Set()))
16 }
17 if (!dep.has(activeEffect)) {
18 dep.add(activeEffect)
19 activeEffect.deps.push(dep)
20 }
21}mutableHandlers的set ProxyHandler定义位于同文件的第125行,调用了同文件的第128行的createGetter。
1const set = /*#__PURE__*/ createSetter()
2
3function createSetter(shallow = false) {
4 return function set(
5 target: object,
6 key: string | symbol,
7 value: unknown,
8 receiver: object
9 ): boolean {
10 const oldValue = (target as any)[key]
11 if (!shallow) {
12 value = toRaw(value)
13 if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
14 oldValue.value = value
15 return true
16 }
17 } else {
18 // in shallow mode, objects are set as-is regardless of reactive or not
19 }
20
21 const hadKey =
22 isArray(target) && isIntegerKey(key)
23 ? Number(key) < target.length
24 : hasOwn(target, key)
25 const result = Reflect.set(target, key, value, receiver)
26 // don't trigger if target is something up in the prototype chain of original
27 if (target === toRaw(receiver)) {
28 if (!hadKey) {
29 trigger(target, TriggerOpTypes.ADD, key, value)
30 } else if (hasChanged(value, oldValue)) {
31 trigger(target, TriggerOpTypes.SET, key, value, oldValue)
32 }
33 }
34 return result
35 }
36}当target为引用类型是set ProxyHandler会根据是否hadKey分别调用TriggerOpTypes类型为SET/ADD类型的trigger。
trigger代码位于@vue/reactivity/src/effect.ts的第167行,是响应的触发器,
1export function trigger(
2 target: object,
3 type: TriggerOpTypes,
4 key?: unknown,
5 newValue?: unknown,
6 oldValue?: unknown,
7 oldTarget?: Map<unknown, unknown> | Set<unknown>
8) {
9 const depsMap = targetMap.get(target)
10 if (!depsMap) {
11 // never been tracked
12 return
13 }
14
15 const effects = new Set<ReactiveEffect>()
16 const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
17 if (effectsToAdd) {
18 effectsToAdd.forEach(effect => {
19 if (effect !== activeEffect || effect.allowRecurse) {
20 effects.add(effect)
21 }
22 })
23 }
24 }
25
26 if (type === TriggerOpTypes.CLEAR) {
27 // collection being cleared
28 // trigger all effects for target
29 depsMap.forEach(add)
30 } else if (key === 'length' && isArray(target)) {
31 depsMap.forEach((dep, key) => {
32 if (key === 'length' || key >= (newValue as number)) {
33 add(dep)
34 }
35 })
36 } else {
37 // schedule runs for SET | ADD | DELETE
38 if (key !== void 0) {
39 add(depsMap.get(key))
40 }
41
42 // also run for iteration key on ADD | DELETE | Map.SET
43 switch (type) {
44 case TriggerOpTypes.ADD:
45 if (!isArray(target)) {
46 add(depsMap.get(ITERATE_KEY))
47 if (isMap(target)) {
48 add(depsMap.get(MAP_KEY_ITERATE_KEY))
49 }
50 } else if (isIntegerKey(key)) {
51 // new index added to array -> length changes
52 add(depsMap.get('length'))
53 }
54 break
55 case TriggerOpTypes.DELETE:
56 if (!isArray(target)) {
57 add(depsMap.get(ITERATE_KEY))
58 if (isMap(target)) {
59 add(depsMap.get(MAP_KEY_ITERATE_KEY))
60 }
61 }
62 break
63 case TriggerOpTypes.SET:
64 if (isMap(target)) {
65 add(depsMap.get(ITERATE_KEY))
66 }
67 break
68 }
69 }
70
71 const run = (effect: ReactiveEffect) => {
72 if (effect.options.scheduler) {
73 effect.options.scheduler(effect)
74 } else {
75 effect()
76 }
77 }
78
79 effects.forEach(run)
80}