0%

vue3响应式实现原理

之前我们分析了 Composition API 的核心 setup 函数的实现,在 setup 函数中,我们多次使用一些 API 让数据变成响应式,那么这次我们就来深入学习和分析响应式内部的实现原理。

响应式它的本质是当数据变化后会自动执行某个函数,映射到组件的实现就是,当数据变化后,会自动触发组件的重新渲染。响应式是 Vue.js 组件化更新渲染的一个核心机制。

Vue.js 2.x的响应式:在内部通过 Object.defineProperty API 劫持数据的变化,在数据被访问的时候收集依赖,然后在数据被修改的时候通知依赖更新。我们用vue2官网的一张图可以直观地看清这个流程。

alt text

在 Vue.js 2.x 中,Watcher 就是依赖,有专门针对组件渲染的 render watcher。这里有两个流程,首先是依赖收集流程,组件在 render 的时候会访问模板中的数据,触发 getter 把 render watcher 作为依赖收集,并和数据建立联系;然后是派发通知流程,当我对这些数据修改的时候,会触发 setter,通知 render watcher 更新,进而触发了组件的重新渲染。

但是Object.defineProperty API 有一些缺点:包括不能监听对象属性新增和删除;初始化阶段递归执行 Object.defineProperty 带来的性能负担。

Vue.js 3.0 为了解决 Object.defineProperty 的这些缺陷,使用 Proxy API 重写了响应式部分,并独立维护和发布整个 reactivity 库,下面就来分析响应式部分的实现原理。

Reactive API

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}

function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
// 目标必须是对象或数组类型
if (!isObject(target)) {
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
// target 已经是 Proxy 对象,直接返回
// 有个例外,如果是 readonly 作用于一个响应式对象,则继续
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
// target 已经有对应的 Proxy 了
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
// 只有在白名单里的数据类型才能变成响应式
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 利用 Proxy 创建响应式
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}

可以看到,reactive 内部通过 createReactiveObject 函数把 target 变成了一个响应式对象。

在这个过程中,createReactiveObject 函数主要做了以下几件事情。

  1. 函数首先判断 target 是不是数组或者对象类型,如果不是则直接返回。所以原始数据 target 必须是对象或者数组。
  2. 如果对一个已经是响应式的对象再次执行 reactive,还应该返回这个响应式对象
  3. 如果对同一个原始数据多次执行 reactive ,那么会返回相同的响应式对象
  4. 只有在白名单里的数据类型才能变成响应式,比如,带有 __v_skip 属性的对象、被冻结的对象,以及不在白名单内的对象如 Date 类型的对象实例是不能变成响应式的。

接下来,我们继续看 Proxy 处理器对象 mutableHandlers 的实现:

1
2
3
4
5
6
7
const mutableHandlers = {
get,
set,
deleteProperty,
has,
ownKeys
}

它其实就是劫持了我们对 observed 对象的一些操作,比如:

访问对象属性会触发 get 函数;

设置对象属性会触发 set 函数;

删除对象属性会触发 deleteProperty 函数;

in 操作符会触发 has 函数;

通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。

因为无论命中哪个处理器函数,它都会做依赖收集和派发通知这两件事其中的一个,所以这里我只要分析常用的 get 和 set 函数就可以了。

依赖收集:get 函数

依赖收集发生在数据访问的阶段,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性被访问的时候就会执行 get 函数,我们来看一下 get 函数的实现流程:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
shallow = this._shallow
if (key === ReactiveFlags.IS_REACTIVE) {
// 代理 __v_isReactive
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 代理 __v_isReadonly
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
// 代理 __v_isShallow
return shallow
} else if (key === ReactiveFlags.RAW) {
// 函数中判断响应式对象是否存在 __v_raw 属性,如果存在就返回这个响应式对象本身。
if (
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
// receiver is not the reactive proxy, but has the same prototype
// this means the reciever is a user proxy of the reactive proxy
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target
}
// early return undefined
return
}

// 是否是数组
const targetIsArray = isArray(target)

if (!isReadonly) {
// arrayInstrumentations 包含对数组一些方法修改的函数
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}

const res = Reflect.get(target, key, receiver)
// 内置 Symbol key 不需要依赖收集
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}

// 依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}

if (shallow) {
return res
}

if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}

// 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}

return res
}

结合上面代码来看,get 函数主要做了四件事情,首先对特殊的 key 做了代理,这就是为什么我们在 createReactiveObject 函数中判断响应式对象是否存在 __v_raw 属性,如果存在就返回这个响应式对象本身。

接着通过 Reflect.get 方法求值,如果 target 是数组且 key 命中了 arrayInstrumentations,则执行对应的函数,我们可以大概看一下 arrayInstrumentations 的实现:

arrayInstrumentations

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
34
35
36
37
38
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()

function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// toRaw 可以把响应式对象转成原始数据,this就是Reflect的receiver
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
// 依赖收集
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling()
resetTracking()
return res
}
})
return instrumentations
}

也就是说,当 target 是一个数组的时候,我们去访问 target.includes、target.indexOf、 target.lastIndexOf 就会执行 arrayInstrumentations 代理的函数,除了调用数组本身的方法求值外,还对数组每个元素做了依赖收集。因为一旦数组的元素被修改,数组的这几个 API 的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化。

当调用 target.push、target.pop、target.shift、target.unshift、target.splice 时候,由于都会访问.length导致收集了数组的length,在某种场景下造成无限循环,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
// https://github.com/vuejs/core/pull/2138

const arr = reactive([])

watchEffect(()=>{
arr.push(1)
})

watchEffect(()=>{
arr.push(2)
})

所以为避免此种情况发生,会暂停收集依赖,在执行完毕后,再恢复依赖收集。

回到 get 函数,第三步就是通过 Reflect.get 求值,然后会执行 track 函数收集依赖,我们稍后重点分析这个过程。

函数最后会对计算的值 res 进行判断,如果它也是数组或对象,则递归执行 reactive 把 res 变成响应式对象。这么做是因为 Proxy 劫持的是对象本身,并不能劫持子对象的变化,这点和 Object.defineProperty API 一致。但是 Object.defineProperty 是在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升。

整个 get 函数最核心的部分其实是执行 track 函数收集依赖,下面我们重点分析这个过程。

track

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 是否应该收集依赖 和 当前激活的 effect
if (shouldTrack && activeEffect) {
// 每个 target 对应一个 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个 key 对应一个 dep Map
depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
}
trackEffect(
activeEffect,
dep,
__DEV__
? {
target,
type,
key,
}
: void 0,
)
}
}

export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
if (dep.get(effect) !== effect._trackId) {
// 收集当前激活的 effect 作为依赖
dep.set(effect, effect._trackId)
const oldDep = effect.deps[effect._depsLength]
if (oldDep !== dep) {
if (oldDep) {
cleanupDepEffect(oldDep, effect)
}
// 当前激活的 effect 收集 dep 集合作为依赖
effect.deps[effect._depsLength++] = dep
} else {
effect._depsLength++
}
if (__DEV__) {
effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
}
}
}

分析track函数的实现前,我们先想一下要收集的依赖是什么,我们的目的是实现响应式,就是当数据变化的时候可以自动做一些事情,比如执行某些函数,所以我们收集的依赖就是数据变化后执行的副作用函数。

再来看实现,我们把 target 作为原始的数据,key 作为访问的属性。我们创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap 的键是 target 的 key,值是 dep 集合,dep 集合中存储的是依赖的副作用函数。为了方便理解,可以通过下图表示它们之间的关系:

../images/vue3响应式实现原理/image-20240316133230452

所以每次 track ,就是把当前激活的副作用函数 activeEffect 作为依赖,然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。

派发通知:set 函数

派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set 函数的实现:

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
34
35
36
37
38
39
40
41
42
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object,
) {
let oldValue = (target as any)[key]
if (!this._shallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
} else {
oldValue.value = value
return true
}
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}

// 检查数组是否包含index或者对象是否有这个属性
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
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
}

结合上述代码来看,set 函数的实现逻辑很简单,主要就做两件事情, 首先通过 Reflect.set 求值 , 然后通过 trigger 函数派发通知 ,并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。

整个 set 函数最核心的部分就是 执行 trigger 函数派发通知 ,下面我们将重点分析这个过程。

我们先来看一下 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
// 通过 targetMap 拿到 target 对应的依赖集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖,直接返回
return
}

// 创建要执行的deps数组
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 清空数组或者map的时候触发所有key对应的的effect函数
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// set add delete操作 将key对应的effect函数添加到deps数组中
if (key !== void 0) {
deps.push(depsMap.get(key))
}

// also run for iteration key on ADD | DELETE | Map.SET
// 根据不同的操作类型push对应的dep
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}

pauseScheduling()
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty,
__DEV__
? {
target,
type,
key,
newValue,
oldValue,
oldTarget,
}
: void 0,
)
}
}
resetScheduling()
}
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
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
pauseScheduling()
// 执行triggerEffects,执行dep里所有的effect
for (const effect of dep.keys()) {
if (
effect._dirtyLevel < dirtyLevel &&
dep.get(effect) === effect._trackId
) {
const lastDirtyLevel = effect._dirtyLevel
effect._dirtyLevel = dirtyLevel
if (lastDirtyLevel === DirtyLevels.NotDirty) {
effect._shouldSchedule = true
if (__DEV__) {
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
effect.trigger()
}
}
}
scheduleEffects(dep)
resetScheduling()
}

所以每次 trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。

ref API

通过前面的分析,我们知道 reactive API 对传入的 target 类型有限制,必须是对象或者数组类型,而对于一些基础类型(比如 String、Number、Boolean)是不支持的。

但是有时候从需求上来说,可能我只希望把一个字符串变成响应式,却不得不封装成一个对象,这样使用上多少有一些不方便,于是 Vue.js 3.0 设计并实现了 ref API。

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
34
35
36
37
38
39
40
41
42
43
44
45
export function ref(value?: unknown) {
return createRef(value, false)
}

function createRef(rawValue: unknown, shallow: boolean) {
// 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
private _value: T
private _rawValue: T

public dep?: Dep = undefined
public readonly __v_isRef = true

constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}

get value() {
// 依赖收集,key 为固定的 value
trackRefValue(this)
return this._value
}

set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
// 判断有变化后更新值
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 派发通知
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}

可以看到,函数首先处理了嵌套 ref 的情况,如果传入的 rawValue 也是 ref,那么直接返回。

接着对 rawValue 做了一层转换,如果 rawValue 是对象或者数组类型,那么把它转换成一个 reactive 对象。

最后定义一个对 value 属性做 getter 和 setter 劫持的对象并返回,get 部分就是执行 track 函数做依赖收集然后返回它的值;set 部分就是设置新值并且执行 trigger 函数派发通知。

总结

最后通过一张图来看一下响应式的工作流程。

alt text

Vue.js 3.0 在响应式的实现思路和 Vue.js 2.x 差别并不大,主要就是 劫持数据的方式改成用 Proxy 实现 , 以及收集的依赖由 watcher 实例变成了组件副作用渲染函数