数据响应式系统

Vue3的数据响应式系统主要由@vue/reactivity承担,主要包括副作用函数effect,依赖收集track和触发更新trigger这三部分。

Composition API实现响应式的关键API为包装基本类型的ref和引用类型的reactive

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}

我们先只关注基本的getset ProxyHandler,其逻辑也比较简单。其中get ProxyHandler除了调用默认的Reflect API,就是调用了track方法。类似的,set ProxyHandler除了调用默认的Reflect API以外,根据目标对象target中是否有要setkey,没有时调用了TriggerOpTypes.ADD类型的trigger方法,已经有key,并且值发生了改变时,调用了TriggerOpTypes.SET类型的trigger方法。

依赖收集与触发

Vue3中的依赖以副作用函数effect的方式体现。副作用函数就是指会产生副作用的函数,就是该函数的执行会直接或者间接影响其他的函数的执行,例如更新DOM,修改作用域以外的变量等。

tracktrigger主要负责依赖的收集(追踪)和触发,类似于Vue2中的Dep,分别在响应式数据getset操作中被执行。用于将响应式数据和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是一个以目标对象targetkeykey、其依赖的集合为valueMap,所以targetMap是一个所有目标对象target的依赖集合的映射总集合。

activeEffect为当前激活状态的effect,具体在effect分析

shouldTrack用于表示是否需要开始依赖收集,trackStack用于标识当前依赖收集的深度。主要用于effect方法中。

track

精简后代码如下:

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,并像activeEffectdeps中加入对于的dep

trigger

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

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分别使用了collectionHandlersbaseHandlers,先看下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:

get

mutableHandlersget``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}

set

mutableHandlersset 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}