Vue3.0Reactivity之readonly

前言

最后一个readonlyAPI了,这个API还是蛮容易理解的。

readonly

老规矩,找到它的定义

1
2
3
4
5
6
7
8
9
10
11
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
readonlyHandlers,
readonlyCollectionHandlers
)
}

这里和我们之前看reactive的实现其实很像,都是主要由另一个函数createReactiveObject来传入参数进行实现

1
2
3
4
5
6
7
8
9
10
11
12
export function reactive(target: object) {
if (readonlyToRaw.has(target)) {
return target
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}

那么我们只要关注参数即可。

这里传入了两个WeakMap,不过这两个WeakMap是建立只读代理到原生对象之间的关系。还传入了两个handlers,这里我们直接找到readonlyHandlers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
has,
ownKeys,
set(target, key) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target, key) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
}

这里我们可以对比reactive使用到的mutableHandlers

1
2
3
4
5
6
7
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}

发现不同的地方在getsetdeleteProperty参数上。

我们先看setdeleteProperty参数,它们直接就在这个对象里面实现。

对于只读的代理对象,当我们尝试对他设置值的时候,set就会在开发环境下打印一个警告,删除属性的操作也是如此。

接下来我们看这个readonly这个函数的实现

1
2
3
4
// mutableHandlers用到的get
const get = /*#__PURE__*/ createGetter()
// readonlyHandlers用到的get
const readonlyGet = /*#__PURE__*/ createGetter(true)

发现其实和mutableHandlers的实现都是通过createGetter这个函数,只不过传入的参数不同而已。

找到这个createGetter函数

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
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)

if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}

if (shallow) {
// 判断不是只读的就收集依赖。
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
}

if (isRef(res)) {
if (targetIsArray) {
// 判断不是只读的就收集依赖。
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays.
return res.value
}
}

// 判断不是只读的就收集依赖。
!isReadonly && track(target, TrackOpTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}

这个函数传入两个参数

  • isReadOnly 是否只读
  • shallow 是否为浅代理

所以其实我们可以发现有几种实现,比如reactiveshallowReactivereadonlyshallowReadonly,就是通过参数的真假组合出来的get所实现的。

这里我们不讲整个流程,我们发现当需要收集依赖,也就是执行track函数的时候,都会先做一个判断,判断传进来的isReadonly的真假。

也就是说,如果一个代理是只读的话,那么就没有必要对它执行依赖收集。因为它的setdeleteProperty没有触发它的依赖,不用触发,自然也就没必要收集。

至此,readonly的实现基本上已经明朗,和reactive的实现有相似之处,都返回了一个Proxy代理对象,不同的是对于getsetdeleteProperty所使用的函数不相同。

通过拦截setdeleteProperty来使得赋值和删除属性无效,也避免了无意义的依赖收集。

后记

今天重新地同步了vue-next的仓库,发现已经出到beta.9版本了,我也发现了一些代码做出了细微的改动,但是万变不离其宗,基本的原理还是一毛一样的。我看到的好像是增加了一些类型声明,还有一些函数的重构。

之后的话会写一些reactivity实现上的一些细节的理解。如果看得懂virtual-dom的话,到时也会写写。