vue3 源码学习:reactive 响应式原理
reactive 功能介绍
根据官方的推荐,reactive 通常用于创建响应式对象或者数组,本质上是对原始对象的代理,所以响应式对象和原始对象是不相等的
1<template>
2 {{ state.count }}
3</template>
4
5<script setup>
6 import { reactive } from 'vue'
7 const raw = {
8 count: 0,
9 name: 'hello'
10 }
11 const state = reactive(raw)
12
13 // 响应式对象和原始对象不相等
14 console.log(state === raw) // false
15 console.log(state === reactive(raw)) // true
16</script>
但是 reactive 使用过程中有两个限制
-
只对对象类型生效(对象、数组、Map、Set),对原始类型无效(string、number)
-
替换或者解构 reactive 对象可能造成响应式丢失,如果需要结构的话需要通过通过
toRefs
保持响应式1const state = reactive({ 2 count: 0, 3 name: 'hello', 4}) 5 6// 结构会导致响应式丢失 7const { count, name } = state 8 9// 使用 toRefs 方法保持响应式 10const { count, name } = toRefs(state)
reactive 实现原理
响应式相关的 api 都定义在 reactivity 这个包下面,reactive
入口函数可以看出,非只读的对象通过 createReactiveObject
方法创建响应式变量
1export function reactive(target: object) {
2 // 如果对象是只读的,直接返回对象
3 if (isReadonly(target)) {
4 return target
5 }
6
7 return createReactiveObject(
8 target,
9 false,
10 mutableHandlers,
11 mutableCollectionHandlers,
12 reactiveMap
13 )
14}
在 createReactiveObject
方法中
- 首先根据传入对象 target 做一系列判断,如果不满足条件则直接返回 target,只有 target 是对象类型、没有被 proxy 代理、在缓存 proxyMap 中不存在、符合 targetType 要求才会使用 Proxy 代理
- 使用 Proxy 代理对象时会根据 targetType 不同使用不同的代理方法,最后把 proxy 后的对象放入缓存并返回即可
1function createReactiveObject(
2 target: Target,
3 isReadonly: boolean,
4 baseHandlers: ProxyHandler<any>,
5 collectionHandlers: ProxyHandler<any>,
6 proxyMap: WeakMap<Target, any>
7) {
8 // 非对象类型直接返回 target
9 if (!isObject(target)) {
10 return target
11 }
12 // 对象已经是 proxy,直接返回
13 if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
14 return target
15 }
16 // 在缓存中存在 proxyMap,直接返回缓存对象
17 const existingProxy = proxyMap.get(target)
18 if (existingProxy) {
19 return existingProxy
20 }
21 // 不符合 target 类型要求,直接返回
22 const targetType = getTargetType(target)
23 if (targetType === TargetType.INVALID) {
24 return target
25 }
26
27 // 核心:根据传入对象的不同类型,使用不同的代理方法,再使用 Proxy 代理
28 const proxy = new Proxy(
29 target,
30 targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
31 )
32 // 设置缓存并返回代理对象
33 proxyMap.set(target, proxy)
34 return proxy
35}
那么 targetType 是如何被定义的,从 TargetType 枚举可以看出,targetType 分别有三类,在通过 getTargetType
获取对象类型时,如果对象被标记了 skip 属性或者不允许添加新属性,则会被标记为 INVALID。然后 Object 和 Array 类型被划分至 COMMON 普通对象,Map、Set、WeakMap、WeakSet 被划分至集合对象
1const enum TargetType {
2 INVALID = 0, // 非法对象
3 COMMON = 1, // 普通对象
4 COLLECTION = 2, // 集合对象
5}
6
7function targetTypeMap(rawType: string) {
8 switch (rawType) {
9 case 'Object':
10 case 'Array':
11 return TargetType.COMMON
12 case 'Map':
13 case 'Set':
14 case 'WeakMap':
15 case 'WeakSet':
16 return TargetType.COLLECTION
17 default:
18 return TargetType.INVALID
19 }
20}
21
22// 获取对象类型
23function getTargetType(value: Target) {
24 return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
25 ? TargetType.INVALID
26 : targetTypeMap(toRawType(value))
27}
再确定了对传入对象 target 类型之后,接下来我们先看普通对象和数组的代理方法定义,根据需要创建的 reactive 类型不同,会区分为如下 4 类
1mutableHandlers // 普通 reactive 代理方法
2shallowReactiveHandlers // 浅响应式 reactive 代理方法
3readonlyHandlers // 只读 reactive 代理方法
4shallowReadonlyHandlers // 浅响应只读 reactive 代理方法
其中 mutableHandlers
定义了完整的 reactive 代理方法,涵盖了对属性的增删改查过程
1export const mutableHandlers: ProxyHandler<object> = {
2 get,
3 set,
4 deleteProperty,
5 has,
6 ownKeys,
7}
get 方法代理
get 代理方法通过 createGetter
方法创建,createGetter
会返回一个 get 函数,从完整的方法函数可以看到,会对类型为普通对象和数组,以及属性 key 不同时有不同的处理方式,具体判断我都写在注释中了
1function createGetter(isReadonly = false, shallow = false) {
2 return function get(target: Target, key: string | symbol, receiver: object) {
3 // 如果属性 key 是 reactive、readonly、shallow,直接返回
4 // 如果 key 是 raw 类型,并且能够从缓存中获取代理结果,直接返回 target
5 if (key === ReactiveFlags.IS_REACTIVE) {
6 return !isReadonly
7 } else if (key === ReactiveFlags.IS_READONLY) {
8 return isReadonly
9 } else if (key === ReactiveFlags.IS_SHALLOW) {
10 return shallow
11 } else if (
12 key === ReactiveFlags.RAW &&
13 receiver ===
14 (isReadonly
15 ? shallow
16 ? shallowReadonlyMap
17 : readonlyMap
18 : shallow
19 ? shallowReactiveMap
20 : reactiveMap
21 ).get(target)
22 ) {
23 return target
24 }
25
26 // 在非只读场景下,处理数组和属性 key 是 hasOwnProperty 时的代理结果
27 const targetIsArray = isArray(target)
28
29 if (!isReadonly) {
30 if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
31 return Reflect.get(arrayInstrumentations, key, receiver)
32 }
33 if (key === 'hasOwnProperty') {
34 return hasOwnProperty
35 }
36 }
37
38 // 获取 get 代理结果
39 const res = Reflect.get(target, key, receiver)
40
41 // 处理 key 是 Symbol 类型场景
42 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
43 return res
44 }
45
46 // 如果对象是非只读,通过 track 方法手机依赖
47 if (!isReadonly) {
48 track(target, TrackOpTypes.GET, key)
49 }
50
51 // 如果对象是浅响应场景,直接返回结果
52 if (shallow) {
53 return res
54 }
55
56 // 如果代理结果是 ref 类型,对于非数组 key 的响应式,返回的是 .value 移除 ref 包裹的结果
57 if (isRef(res)) {
58 return targetIsArray && isIntegerKey(key) ? res : res.value
59 }
60
61 // 如果代理结果是 ref 类型,在非只读情况下递归调用 reactive 方法将整个对象都设置为响应式
62 if (isObject(res)) {
63 return isReadonly ? readonly(res) : reactive(res)
64 }
65
66 return res
67 }
68}
但是对 createGetter
方法进行简化后,可以看到核心部分只有 4 行代码,通过 Reflect.get
获取代理结果,通过 tarck
方法追踪函数的副作用,简化后的 createGetter
方法如下
1function createGetter(isReadonly = false, shallow = false) {
2 const res = Reflect.get(target, key, receiver)
3
4 track(target, TrackOpTypes.GET, key)
5
6 return res
7}
在介绍 track
方法之前,先要说明一下响应式依赖的存储数据结构 targetMap,targetMap 自身是一个 weakMap,键是原始对象,值是 map 实例;map 实例存储的键是原始对象的 key,值是由副作用函数组成的 set 集合。这样就形成了一个 target -> key -> effect 的结构,我们只要通过代理对象 target 和 key 就能够拿到全部的副作用函数
另外为什么 targetMap 使用的是 weakMap 而不是普通 Map,因为 weakMap 是弱引用,当 weakMap 的 key 没有引用时, 垃圾回收机制会将 key 和 value 从内存中移除。当对象 target 没有任何响应式依赖时,说明不需要再对 target 作响应式追踪,及时移除能够避免内存溢出

在 track
方法中, 当 shouldTrack 为 true,activeEffect 副作用函数存在时才收集依赖,从 targetMap 中根据 target 和 key 获取响应式依赖并交给 trackEffects
处理。在 trackEffects
方法中,在满足条件的情况下,将依赖放入集合中
1export function track(target: object, type: TrackOpTypes, key: unknown) {
2 // 当 shouldTrack 为 true,activeEffect 副作用函数存在时才收集依赖
3 if (shouldTrack && activeEffect) {
4 // 从 targetMap 中根据对象 key 获取对应的副作用函数,获取不到直接创建
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 = createDep()))
12 }
13
14 // 将 dep 副作用函数集合交给 trackEffects 执行
15 trackEffects(dep)
16 }
17}
18
19export function trackEffects(dep: Dep) {
20 let shouldTrack = false
21 // 在没有超过依赖递归最大深度的情况下,新添加追踪的依赖才收集
22 if (effectTrackDepth <= maxMarkerBits) {
23 if (!newTracked(dep)) {
24 dep.n |= trackOpBit
25 shouldTrack = !wasTracked(dep)
26 }
27 } else {
28 // 超过依赖递归最大深度,需要追踪依赖
29 shouldTrack = !dep.has(activeEffect!)
30 }
31
32 // 如果应该收集依赖,则将 activeEffect 副作用函数添加到 dep 副作用函数集合中
33 if (shouldTrack) {
34 dep.add(activeEffect!)
35 activeEffect!.deps.push(dep)
36 }
37}
另外要单独说一下对于数组的 get 方法处理,对于数组的操作方法主要分
- 数组查询方法,不会改变数组长度,比如
includes
、indexOf
方法 - 操作数组方法,会改变数组长度,比如
push
、pop
等方法
对于这两类操作,在 vue3 中通过直接重写数组方法 createArrayInstrumentations
- 对于数组查询类方法,会遍历数组并对数组的每一项做响应式追踪,然后先从 this 指向的代理对象查询,查询不到再从 this.raw 指向的原数组查询
- 而对于操作数组的方法,因为操作过程会读取数组的 length 属性,造成副作用函数的死循环,所以在执行数组操作方法,先禁止 track,操作完成后再恢复
1function createArrayInstrumentations() {
2 const instrumentations: Record<string, Function> = {}
3 // 数组查询方法
4 ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach((key) => {
5 instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
6 const arr = toRaw(this) as any
7 for (let i = 0, l = this.length; i < l; i++) {
8 track(arr, TrackOpTypes.GET, i + '')
9 }
10 // 执行数组查询
11 const res = arr[key](...args)
12 if (res === -1 || res === false) {
13 return arr[key](...args.map(toRaw))
14 } else {
15 return res
16 }
17 }
18 })
19 // 数组操作方法
20 ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key) => {
21 instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
22 // 执行操作方法前先停止 track
23 pauseTracking()
24 // 执行数组操作方法
25 const res = (toRaw(this) as any)[key].apply(this, args)
26 // 恢复 track
27 resetTracking()
28 return res
29 }
30 })
31 return instrumentations
32}
set 方法代理
set 代理方法通过 createSetter
方法创建,createSetter
方法同样会对 readonly、shallow 等场景做处理,下面只列最核心的部分。首先排除掉 target 的原型链也是 proxy 的情况,避免二次触发 setter,再根据 key 是否存在于 target 中,执行新增属性还是修改属性 trigger
方法
1function createSetter(shallow = false) {
2 return function set(
3 target: object,
4 key: string | symbol,
5 value: unknown,
6 receiver: object
7 ): boolean {
8 // 判断 key 是否存在于 target 中
9 const hadKey =
10 isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
11 // set 方法的执行结果
12 const result = Reflect.set(target, key, value, receiver)
13
14 // target 的原型链也是 proxy 的情况下,通过 Reflect.set 修改原型链上的属性会再次出发 setter
15 // 所以此时就不用触发 trigger 了
16 if (target === toRaw(receiver)) {
17 if (!hadKey) {
18 trigger(target, TriggerOpTypes.ADD, key, value)
19 } else if (hasChanged(value, oldValue)) {
20 trigger(target, TriggerOpTypes.SET, key, value, oldValue)
21 }
22 }
23 return result
24 }
25}
trigger
函数会从 targetMap 中获取响应式依赖集合,也就是需要执行的副作用函数,然后通过 triggerEffects 方法执行获取到的副作用函数
1export function trigger(/*相关参数*/) {
2 const depsMap = targetMap.get(target)
3 // targetMap 中没有说明没有追踪依赖,直接返回
4 if (!depsMap) {
5 return
6 }
7
8 let deps: (Dep | undefined)[] = []
9 // key 为 length 表示对数组的依赖执行
10 if (key === 'length' && isArray(target)) {
11 const newLength = Number(newValue)
12 depsMap.forEach((dep, key) => {
13 if (key === 'length' || key >= newLength) {
14 deps.push(dep)
15 }
16 })
17 } else {
18 // 获取对象中的依赖函数
19 if (key !== void 0) {
20 deps.push(depsMap.get(key))
21 }
22 }
23
24 // 通过 triggerEffects 执行副作用函数
25 if (deps.length === 1) {
26 if (deps[0]) {
27 triggerEffects(deps[0])
28 }
29 } else {
30 const effects: ReactiveEffect[] = []
31 for (const dep of deps) {
32 if (dep) {
33 effects.push(...dep)
34 }
35 }
36 triggerEffects(createDep(effects))
37 }
38}
在 triggerEffects
函数中,会把传入的副作用函数集合转化为数组统一遍历,然后先执行 computed 类型的副作用函数,因为 computed 执行过程中可能会产生新的副作用函数,最后再执行非 computed 类型的副作用函数,所有副作用函数都通过 triggerEffect
函数执行
triggerEffect
执行过程中会判断副作用函数是否有 scheduler 调度器,有的话直接执行调度器,没有的话直接通过 run
方法执行副作用函数
1export function triggerEffects(dep: Dep | ReactiveEffect[]) {
2 // 将副作用函数转换为函数统一遍历
3 const effects = isArray(dep) ? dep : [...dep]
4 // 首先执行 computed 类型的副作用函数,因为 computed 执行过程中可能会产生新的副作用函数
5 for (const effect of effects) {
6 if (effect.computed) {
7 triggerEffect(effect)
8 }
9 }
10 // 执行非 computed 类型的副作用函数
11 for (const effect of effects) {
12 if (!effect.computed) {
13 triggerEffect(effect)
14 }
15 }
16}
17
18function triggerEffect(effect: ReactiveEffect) {
19 if (effect !== activeEffect || effect.allowRecurse) {
20 if (effect.scheduler) {
21 effect.scheduler()
22 } else {
23 effect.run()
24 }
25 }
26}
deleteProperty、has、ownKeys 方法代理
这三个方法也是在对属性做删除、查询属性、查询属性 key 时,做响应式操作。deleteProperty
方法在删除属性时执行 trigger
执行副作用函数,has
和 ownKeys
查找属性时执行 track
收集副作用函数
1function deleteProperty(target: object, key: string | symbol): boolean {
2 const hadKey = hasOwn(target, key)
3 const oldValue = (target as any)[key]
4 const result = Reflect.deleteProperty(target, key)
5 if (result && hadKey) {
6 trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
7 }
8 return result
9}
10
11function has(target: object, key: string | symbol): boolean {
12 const result = Reflect.has(target, key)
13 if (!isSymbol(key) || !builtInSymbols.has(key)) {
14 track(target, TrackOpTypes.HAS, key)
15 }
16 return result
17}
18
19function ownKeys(target: object): (string | symbol)[] {
20 track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
21 return Reflect.ownKeys(target)
22}
reactive 原理回顾
最后我们来整理一下 reactive 方法整体的实现原理,整体一共分为三步
- 根据代理对象的类型,通过 Proxy 创建代理
- 定义对象操作的不同代理对象,主要是 get 读取操作和 set 编辑操作
- get 操作时通过
track
方法收集副作用函数,set 操作时通过trigger
方法执行副作用函数