Vue 3 原理解析 - 数据侦测原理

Vue 3 原理解析 - 数据侦测原理

Vue 2.x 及之前的版本中实现数据的可响应,需要对 ObjectArray 分别进行处理:

  • Object 类型通过 Object.definePropery 把属性转换成 getter/setter ,这个过程需要递归侦测所有的对象 key 来实现深度侦测
  • Array 类型通过对改变数组自身的几个方法进行拦截来实现对数组的可响应

而在 Vue3 中则是通过 Proxy 实现数据读取和设置拦截,在捕捉器中实现数据依赖收集和触发视图更新的操作。

Proxy

Proxy 是 ES6 中新引入的特性。

Proxy 正如其含字面意义 - “代理” 所表明的那样,它是对象与对象之间的一层代理,程序可以通过 Proxy 来访问或操作目标对象,进而可以实现基本操作的拦截和自定义。

基本语法如下:

1
const proxy = new Proxy(target, handler);

参数:

  • target 要代理的原始对象,可以是任何类型的对象,包括原生数组、函数甚至另一个代理对象
  • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为

举个栗子:

1
2
3
4
5
let const obj = {};
const proxyHandler = {};
const objProxy = new Proxy(obj, proxyHandler);
// 赋值操作被转发到 obj 对象
objProxy.a = 37;

handler 中可以设置对原始对象各种操作的捕获器,在捕获器中可以自定义修改操作的行为。

1
2
3
4
5
6
7
const obj = { a: 'A' };
const proxy = new Proxy(obj, {
get: (o, p) => {
if (p === 'a') return 'This is A';
return o[p];
}
});

有时我们并不知道目标对象的具体类型,这种情况下使用 Reflect 返回 trap (捕捉器)相应的默认行为。

Reflect 是一个内置的不可构造的非函数对象,它提供了拦截 JavaScript 各种操作的方法。

1
2
3
4
5
6
7
8
9
10
let target = {};
const proxyObj = new Proxy(target, {
get: (obj, prop) => {
return Reflect.get(obj, prop, receiver);
},
set: (obj, prop, value) => {
Reflect.set(obj, prop, value, receiver);
return true;
}
});

对于 set 操作,可能会引起代理对象的属性更改,导致 set 多次执行。比如当代理对象是数组时,执行 push 操作,会多次触发 set,同时也引发 get 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let array = [ 1, 3 ];

const proxyHandler = {
get: (target, prop, receiver) => {
console.log('Get value : ', prop, Reflect.get(target, prop, receiver));
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value, receiver) => {
console.log('Set value :', prop, value);
return Reflect.set(target, prop, value, receiver);
}
};

const objProxy = new Proxy(array, proxyHandler);

objProxy.push(4);

// Get value : push [Function: push]
// Get value : length 2
// Set value : 2 4
// Set value : length 3

当要代理的对象是多层结构时,Proxy 的代理只能到第一层,即不能感知操作对象内部的 set 操作,但是 get 会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = { 'foo': 'foo', 'bar': { 'name': 1 } };

const proxyHandler = {
get: (target, prop, receiver) => {
console.log('Get value : ', prop, Reflect.get(target, prop, receiver));
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value, receiver) => {
console.log('Set value :', prop, value);
Reflect.set(target, prop, value, receiver);
return true;
}
};

const objProxy = new Proxy(obj, proxyHandler);

console.log(objProxy.bar.name);

// Get value : bar { name: 1 }
// 1

一句话总结:Proxy 是 ES6 引入的新特性,可以使用 Proxy 间接访问或操作其代理的对象。

Vue 3 中响应式数据实现

Vue 3 中响应式系统的 API 主要有

  • reactive
  • ref
  • computed
  • readonly
  • watchEffect
  • watch

其中 reactive 是最核心的 API

reactive

Vue 3 中是用全局的 WeakMap 来存储正在追踪的响应式对象,如 reactiveMapshallowReactiveMapreadonlyMapshallowReadonlyMap

1
2
3
4
export const reactiveMap = new WeakMap<Target, any>()
export const shallowReactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()

此外还定义了代理对象的可响应操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 普通对象,如 Object 、Array 的拦截器
// 包含了当对代理对象进行取值、复制、删除属性等操作时的捕获器
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
};

// 集合对象,如果 Map/WeakMap/Set/WeakSet 的拦截器
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

reactive 创建响应式对象的流程如下:

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
/**
* 生成一个原始对象的响应式副本,这种转换是深层次的,会影响到所有嵌套属性
*/
function reactive(target: object) {
// 如果目标对象是只读的,直接返回目标对象
if (target && (target as Target).__v_isReadonly) {
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> // 正在追踪的可响应对象的集合
) {
// 如果 target 不是对象类型,返回原值
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 如果 taret 已经是一个响应式对象,返回原值
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 如果 target 已经有一个代理对象,返回已经存在的代理对象
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有特定类型的对象才可以被观测
// 包括 Array/Object/Map/Set/WeakMap/WeakSet,不是则返回 TargetType.INVALID
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
// Map/Set/WeakMap/WeakSet 被认为是 COLLECTION 类型,handler 和其它类型不同
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}

上面提到了对于多层对象的代理,set 并不能感知到内层对象的变化,但是 get 会被触发,Vue 3 利用这个原理,再对内层数据进行一次代理。

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
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
// 如果 key 是可响应标志 IS_REACTIVE,返回 !isReadonly
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 如果 key 是只读标志 IS_READONLY,返回 isReadonly
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}

const targetIsArray = isArray(target)

// 如果目标是数组
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}

const res = Reflect.get(target, key, receiver)

// 判断 key 是否是 Symbol
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}

// 如果可读,追踪
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}

// 如果是浅层响应,直接返回得到的值
if (shallow) {
return res
}

// 判断是否 Ref
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : 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
}
}

使用 reactive 创建了响应式对象后,改变响应式对象的属性操作,会被侦测,并即时地在目标对象上触发对应的响应。

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
43
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)

function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 取得旧的值 oldValue
let oldValue = (target as any)[key]

// 如果不是浅层响应模式,需要进行一系列的判断,才能赋值
if (!shallow) {
value = toRaw(value)
oldValue = toRaw(oldValue)
// 如果 target 不是数组,旧值是 Ref
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}

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
if (target === toRaw(receiver)) {
// 在 target 上触发 ADD 或者 SET 响应
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}

ref

由于 Proxy 代理的目标需是对象形式,不能对简单类型进行代理,为此 Vue 3 中使用 ref 函数为简单类型的值生成了一个包装,这样就可以通过 recative 函数构建响应式数据了。

包装对象的实现如下:

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
class RefImpl<T> {
private _value: T
private _rawValue: T

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

constructor(value: T, public readonly _shallow = false) {
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : convert(value)
}

get value() {
trackRefValue(this)
return this._value
}

set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
triggerRefValue(this, newVal)
}
}
}
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
interface Ref<T = any> {
[RefSymbol]: true
value: T
}

function ref(value?: unknown) {
return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
// 判断是否是 Ref,如果是 Ref 则直接返回
if (isRef(rawValue)) {
return rawValue
}
// 判断是否是浅代理
// convert = (val) => isObject(val) ? reactive(val) : val
let value = shallow ? rawValue : convert(rawValue)

//
const r = {
__v_isRef: true,
get value() {
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
trigger(
r,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
}
return r
}

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
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
function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>

// 如果传入的 getterOrOptions 是函数,则将其设置为计算属性的 getter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
// 如果只传入了 getter 函数,将计算属性是不可修改的 readonly
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
// 如果 getterOrOptions 是个对象,则计算属性是可修改的
getter = getterOrOptions.get
setter = getterOrOptions.set
}

let dirty = true
let value: T
let computed: ComputedRef<T>

const runner = effect(getter, {
lazy: true,
// 将 effect 标记为 computed,则其在执行时拥有更高的优先级
computed: true,
scheduler: () => {
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})

// 构建一个 computed 对象
computed = {
__v_isRef: true,
// 将 effect 暴露,使得计算属性能够 stop
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
// 追踪
track(computed, TrackOpTypes.GET, 'value')
// 返回最新值
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any

return computed
}

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}

function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
// 如果 effectStack 不存在当前 effect
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
// 将当前 effect 加入 effectStack
effectStack.push(effect)
// 将当前 effect 设置为 activeEffect
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
关于 triggertrack

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
88
89
90
91
function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// targetMap 存储了所有被追踪的对象,如果当前对象没有被追踪,直接返回
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}

const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}

// 如果当前触发类型为 CLEAR
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}

const run = (effect: ReactiveEffect) => {
// 在开发环境下,调试触发器
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}

// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
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
function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
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)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}