Vue3.x源码阅读笔记(五)-响应式原理
在Vue3中,我们如果使用组合式API,那么就需要我们自己通过reactive模块,告诉Vue,哪些数据是需要变为响应式。
reactive
1 | |
来看下reactive的具体实现。首先判断如果是一个readonly对象,就直接返回,因为readonly对象不可以被set,也就不需要去创建响应式对象了。然后通过createReactiveObject这个方法去创建一个响应式对象。
1 | |
createReactiveObject
这个方法就是用来创建响应式对象,首先判断如果target不是一个对象,那么会报一个警告并且返回,target必须是一个对象或者数组。
然后判断如果已经被代理过,那么也直接返回当前target
然后如果当前的代理集合中已经存在能获取到,也直接返回当前被代理的对象
最后做了一个检测,如果是带有 __v_skip 属性的对象、被冻结的对象,或者不在白名单的对象,比如Date类型对象等不可以变成响应式对象, Vue3 仅会对 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其他对象会被标记为 INVALID,
最后通过Proxy将target劫持,将其变成响应式对象。这里有一个判断,通过targetType来决定是使用collectionHandlers还是使用baseHandlers。
最后设置到proxyMap中,proxyMap存储生成代理对象后的Map对象,再返回创建好的proxy
1 | |
new Proxy
然后通过new proxy创建一个代理对象返回
1 | |
这里的targetTyp是上面判断出来的数据类型,如果是2,那么就是Map,Set,WeakMap,WeakSet这几个类型,用collectionHandlers,当target 类型是 Array 或 Object 时,就是baseHandlers
1 | |
简单来讲,mutableHandlers就是会劫持我们的对象操作。
- 访问对象属性会触发
get函数; - 设置对象属性会触发
set函数; - 删除对象属性会触发
deleteProperty函数; in操作符会触发has函数;- 通过
Object.getOwnPropertyNames访问对象属性名会触发ownKeys函数。当我们触发上面的这些不管哪个,都会做依赖搜集和派发更新这两个操作中的一个。1
2
3
4
5
6
7
8
9
10
11
12//packages/reactivity/src/baseHandlers.ts
/**
* 劫持对对象的操作,访问会触发get,设置会触发set,删除处罚delete,in触发has
* Object.getOwnPropertyNames 触发 ownKeys 函数
*/
export const mutableHandlers: ProxyHandler<object> = {
get, // createGetter()
set, // createSetter()
deleteProperty,
has,
ownKeys
}依赖收集:createGetter | get 函数
get函数通过createGetter创建。
这个函数首先对key做了判断, 如果get访问的key是__v_isReactive,返回createGetter的isReadonly参数取反结果,如果get访问的key是__v_isReadonly,返回createGetter的isReadonly参数,如果get访问的key是__v_raw,并且receiver与原始标识相等,则返回原始值。在后面就是对当前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
92
93
94
95
96
97
98
99// packages/reactivity/src/baseHandlers.ts
const get = /*#__PURE__*/ createGetter()
// 依赖搜集get函数
// 依赖搜集发生在数据访问阶段,被访问时候触发get函数
/**
* isReadonly:只读
* shallow: 是否是一个对象
*
*/
/**
*
* target如果是数组,比如a = reactive([1]);a.push(2)时候,此时的key就是push,然后会再回到arrayInstrumentations方法中再次触发get,此时key是length
*/
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// 对特殊的key做处理
/**
* key是length表示数组长度发生了变化
*/
if (key === ReactiveFlags.IS_REACTIVE) {// "__v_isReactive"
// 代理 observed.__v_isReactive,判断是不是reactive 对象
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {// "__v_isReadonly"
// 代理 observed.__v_isReadonly,判断是不是readonly 对象
return isReadonly
} else if (
// 代理 observed.__v_raw
key === ReactiveFlags.RAW && // "__v_raw"
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target) // 仅限于reactive([1])这种数组情况
// 不是只读 是数组,且 key 命中了 arrayInstrumentations
// 如果target是一个数组,当我们去访问'push', 'pop', 'shift', 'unshift', 'splice','includes', 'indexOf', 'lastIndexOf'会触发依赖搜集
/**
* arrayInstrumentations = {
* includes: ƒ (...args)
indexOf: ƒ (...args)
lastIndexOf: ƒ (...args)
pop: ƒ (...args)
push: ƒ (...args)
shift: ƒ (...args)
splice: ƒ (...args)
unshift: ƒ (...args)
* }
*/
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 获取到对应的方法返回,比如.push时候执行的就会是被代理过的方法
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 获取当前值
const res = Reflect.get(target, key, receiver)
// 内置元素symbol 不需要依赖搜集
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 如果不是只读,依赖搜集
// 只读属性不会被修改,所以不用跟踪变化
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 只对最外层响应式,不执行嵌套对象的响应式转换
/**
* o:{
* a:{
* b: {}
* }
* }
*/
if (shallow) {
return res
}
// __v_isRef
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 对拿到的结果,执行嵌套对象的响应式转换
// 如果子值依然是对象,递归处理为响应式对象
// 这里和Object.defineProperty API一样,不能劫持子对象的变化,但是Object.defineProperty API是在开始就遍历了data中的数据, Proxy是在对象被访问时候才去递归执行下一步 reactive,性能上会有提升,这里获取到值是一个对象的话就去遍历转换
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res) // 递归reactive
}
return res
}
}target是数组情况做了判断,如果是一个数组,并且不是只读,arrayInstrumentations代理了includes ,indexOf,lastIndexOf,pop,push,shift,splice,unshift几个方法,执行数组这个几个方法会走到被代理过的方法里面,有点类似Vue2中的数组劫持。这里为什么是这几个方法呢?
因为一旦数组的元素被修改,数组的这几个
API的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化(includes、indexOf、lastIndexOf)会改变自身长度的数组方法,需要避免 length 被依赖收集,因为这样可能会造成循环引用
(push、pop、shift、unshift、splice)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
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
// 把数据转换成原始数据
const arr = toRaw(this)
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 = method.apply(arr, args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = method.apply(this, args)
resetTracking()
return res
}
})然后通过
Reflect求值,执行track函数搜集依赖。1
2
3
4
5// 如果不是只读,依赖搜集
// 只读属性不会被修改,所以不用跟踪变化
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}track
首先判断
activeEffect当前的副作用函数不存在或者shouldTrack是否可以被依赖搜集的标志位不存在,那就直接返回。
这里我们比如写在模板中时候{{state}},那么在编译环节,生成的render函数就会访问这个对象,从而触发依赖搜集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/**
* 模板中写的{{state}} 编译成渲染函数会访问这里,触发依赖搜集,模板中写的才会触发这里
* const state = Vue.reactive({
msg: 'msg reactive',
arr:[],
deepObj:{
test:{a:1}
},
deepObj2:{
test:{a:1}
}
})
模板中访问{{ state.msg }}
此时的target,type,key分别为
第一次 target: {"msg":"msg reactive","arr":[],"deepObj":{"test":{"a":1}},"deepObj2":{"test":{"a":1}}},type:get,key:msg
模板中访问{{state.deepObj.test}},里面的每一个对象都会被触发一次
触发 track -> target: {"msg":"msg reactive","arr":[],"deepObj":{"test":{"a":1}},"deepObj2":{"test":{"a":1}}} type:get key deepObj
触发 track -> target: {"test":{"a":1}} type:get key test
触发 track -> target: {"a":1} type:get key toJSON
触发 track -> target: {"a":1} type:iterate key Symbol(iterate)
触发 track -> target: {"a":1} type:get key a
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
/**
* shouldTrack 是否应该搜集依赖,我们push方法或者别的执行,会把shouldTrack设置为true
* activeEffect 当前激活的 effect
*/
// 比如我们只写了一个console.log(state.num),这里到下面就会结束,不会再触发依赖搜集
if (!shouldTrack || activeEffect === undefined) {
return
}
// const targetMap = new WeakMap<any, KeyToDepMap>()
let depsMap = targetMap.get(target)
if (!depsMap) {
// 没有被搜集过,每一个target都对应一个Map
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个 key 对应一个 dep 集合
depsMap.set(key, (dep = new Set()))
}
// 依赖搜集就是搜集数据变化后执行的副作用函数
/**
* target:{
* key1:[effect1,effect2],
* key2:[effect1,effect2]
* }
* 比如number.a
* number(target):{
* a(key):[effect1,effect2](depsMap)
* }
*/
if (!dep.has(activeEffect)) { // activeEffect 当前激活的副作用函数,数据变化后执行的副作用函数。
// 搜集当前的effect函数 作为依赖
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}然后保存一个这种格式。每次
track,把当前激活的activeEffect作为依赖,搜集到target相关的depsMap对应key下的依赖集合dep中1
2
3
4
5targetMap: WeakMap{
target:Map{
key: Set[effect1,effect2...]
}
}派发更新:createSetter | set函数
当我们去修改值的时候,会触发派发通知,因为被
proxy代理,所以会执行set函数。set函数主要逻辑是通过Reflect.set求值,然后通过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// packages/reactivity/src/baseHandlers.ts
const set = /*#__PURE__*/ createSetter()
// 派发通知发生在数据更新的阶段
// 响应式对象属性更新会触发set函数
function createSetter(shallow = false) {
return function set(
target, //数组的话就是我们的那个数组
key, // 如果是数组push,key就是当前数组长度length字符串,比如 '3',pop这里的key是length,splice的key是'0'
value, // 设置的新值
receiver
): boolean {
// 获取到当前的值
let oldValue = (target as any)[key]
// 如果不是浅代理,证明当前对象里面所有的都是响应式的深层代理
if (!shallow) {
// 这里需要toRaw原因是传入的有可能是响应式对象
value = toRaw(value) // 将新的值,设置的响应式值转换为普通值
oldValue = toRaw(oldValue) //将旧的值,设置的响应式值转换为普通值
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 目标对象不是数组,旧值是ref(已经被reactive包装过),新值不是ref,直接赋值
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) // 判断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)) {
// trigger 用来做派发通知
if (!hadKey) { // 新增值
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) { // 修改值
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}首先判断如果老的值已经是一个
Ref响应式对象,那么我们直接赋值即可,因为老的值已经是被ref,我们重新赋值,就会触发set函数,所以这里直接return,不需要去trigger1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 获取到当前的值
let oldValue = (target as any)[key]
// 如果不是浅代理,证明当前对象里面所有的都是响应式的深层代理
if (!shallow) {
// 这里需要toRaw原因是传入的有可能是响应式对象
value = toRaw(value) // 将新的值,设置的响应式值转换为普通值
oldValue = toRaw(oldValue) //将旧的值,设置的响应式值转换为普通值
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 目标对象不是数组,旧值是ref(已经被reactive包装过),新值不是ref,直接赋值
// 这里为什么不需要去trigger呢?因为老的值已经是被ref,我们重新赋值,就会触发
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}然后判断当前我们对这个值是新增还是修改,然后触发
trigger函数派发通知1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length // 数组的话,判断新的长度和旧的长度
: hasOwn(target, key) // 判断target和key,判断是新增还是修改值
// 设置值
const result = Reflect.set(target, key, value, receiver)
// 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
if (target === toRaw(receiver)) {
// trigger 用来做派发通知
if (!hadKey) { // 新增值
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) { // 修改值
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return resulttrigger
trigger函数主要做以下几个事情
首先获取从
targetMap获取depsMap集合,这个集合已经在track时候依赖搜集起来。创建
effects集合根据
key从depsMap从获取当前的effect集合并且通过add函数添加遍历
effect执行相关的副作用函数
1 | |
readonly
在Vue中,我们可以通过readonly来实现把一个对象变为只读,因为如果我们使用const的话,还是可以修改对象中的属性的。
1 | |
看下里面的具体实现。也是基于createReactiveObject实现的
createReactiveObject
1 | |
和reactive中的有区别的就是baseHandlers,collectionHandlers的传入,因为readonly不允许对象的修改和删除。这里的baseHandlers是readonlyHandlers,collectionHandlers是readonlyCollectionHandlers
1 | |
来看下createReactiveObject的readonlyCollectionHandlers
1 | |
当我们对其进行set和delete时候,会报一个警告,禁止我们去设置和删除值。
这里的get是readonlyGet,从createGetter方法创建的
1 | |
1 | |
这里和reactive中的createGetter最大的区别是,不需要再去track执行依赖搜集,因为是只读的,并不会去修改他,也就不需要跟踪变化了。
ref
在Vue中,可以通过ref将一个基础数据类型转换为一个响应式对象。因为reactive API会有一个判读,必须是Object或者Array
1 | |
这里是通过createRef创建返回的。
createRef
1 | |
首先通过isRef对当前的值做了判断,如果已经是一个响应式对象,那么就返回这个响应式对象。否则就去通过new RefImp实例化
1 | |
new RefImpl
当我们去实例化RefImpl时候,会先调用constructor中的代码。
1 | |
首先通过_shallow做了一次判断,决定采用传入的原始值还是调用convert处理。convert很简单,就是判读当前的值如果是数组或者对象,那么就用reactive,否则就返回当前的val。
1 | |
再然后,就是定义了get和set函数。当我们访问数据会触发get,从而触发track函数进行依赖搜集。
1 | |
当我们设置值时候会触发set函数,这里首先通过hasChanged判断一个值是否有更改,通过判断了NaN的情况。
1 | |
如果新的值有更新,就会通过trigger来去派发通知。
1 | |
副作用函数
在trigger和track中我们可以看到,都离不开effect。那么我们来看下effect相关的内容。
1 | |
这个函数的主要作用是通过createReactiveEffect去创建一个叫做reactiveEffect()的副作用函数,将当前的副作用函数压入栈,并且将effect赋值给全局的activeEffect,这里在依赖搜集时候会把当前的activeEffect搜集起来, 然后执行原始函数。
createReactiveEffect
1 | |
本文标题:Vue3.x源码阅读笔记(五)-响应式原理
文章作者:Niuhk
发布时间:2022-07-16
最后更新:2022-07-23
原始链接:https://www.niuhk.cn/2022/07/16/Vue3.x源码阅读笔记(五)-响应式原理/
版权声明:转载请注明出处!
分享