vue-reactivity readonly 阅读

通过 readonly 可以设置一些只读的值,

1
2
3
4
5
6
7
8
9
const original = { foo: 1, bar: { baz: 2 } }
const wrapped = readonly(original)

effect(()=>{
console.log(warpped)
})

// not effact
warpped.foo = 2

readonly 是对 createReactiveObject 的封装

1
2
3
4
5
6
7
8
9
10
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true, // isReadonly 标记
readonlyHandlers, // 一般类型的处理 readonly 处理函数
readonlyCollectionHandlers // 对于set map 等集合类型的处理函数
)
}

readonlyHandlersreadonlyCollectionHandlers

readonlyHandlers 相对简单,主要是在 setdeleteProperty 阶段,不对 target 进行处理,而直接返回 true

另外如果是开发环境,则输出相应的的提示

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
}
}

readonlyGet 也是通过 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
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
const readonlyGet = /*#__PURE__*/ createGetter(true)


function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// 如果获取的 key 是 isReactive 的 flag,
// 则根据是否 isReadonly 取反
if (key === ReactiveFlags.isReactive) {
return !isReadonly

// 如果获取的 key 是 isReadonly 的 flag,
// 则根据是否 isReadonly 返回布尔值
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
// 如果获取的 key 是 raw 的 flag,
// 则根据是否 isReadonly 判断 receiver
// 和__v_readonly或者__v_reactive 相等,来决定是否返回 target
} else if (
key === ReactiveFlags.raw &&
receiver ===
(isReadonly
? (target as any).__v_readonly
: (target as any).__v_reactive)
) {
return target
}

const targetIsArray = isArray(target)
// 如果 target 是 数组则判断 key 是否是 'includes', 'indexOf', 'lastIndexOf' 之一
// 如果是则通过 Refect.get 直接返回,对应的方法改写
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}


const res = Reflect.get(target, key, receiver)
// 如果是 symbol 或者 内置 builtInSymbols或原型链的key, 则直接返回对应的的 target key 值
if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') {
return res
}
// 如果不是 isReadonly 则触发一次 get 的 track
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}

if (shallow) {
return res
}

if (isRef(res)) {
// ref unwrapping, only for Objects, not for Arrays.
return targetIsArray ? res : res.value
}

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
}
}

readonlyCollectionHandlers 只代理 get 方法,这个因为对应 Map Set 等类型其实通过 调用对应的函数来处理数据,而不是直接 set

1
2
3
4
5
6
7
const set = new Set()

// 调用 `add`函数相当于对 `add` 关键字进行 get 操作,因此只需要代理 get
set.add(1)



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
export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(true, false)
}

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
//此处根据构建状态返回不同的处理 `shallow` `isReadonly` 或者正常状态的代理逻辑
const instrumentations = shallow
? shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations

return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
// 还是一样对特殊 flag 做处理
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (key === ReactiveFlags.raw) {
return target
}

// 最后根据 key 代理到对应的 `instrumentations` 中
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}

着重看 readonlyInstrumentations

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
const readonlyInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, toReadonly)
},
get size() {
return size((this as unknown) as IterableCollections)
},
has,
add: createReadonlyMethod(TriggerOpTypes.ADD),
set: createReadonlyMethod(TriggerOpTypes.SET),
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
forEach: createForEach(true, false)
}


// 对于会改写数据的方法,通过工厂函数进行覆盖
// 关键依然是让这些方法不对 target 做任何操作直接返回,达到 readonly 的效果
function createReadonlyMethod(type: TriggerOpTypes): Function {
return function(this: CollectionTypes, ...args: unknown[]) {
if (__DEV__) {
const key = args[0] ? `on key "${args[0]}" ` : ``
console.warn(
`${capitalize(type)} operation ${key}failed: target is readonly.`,
toRaw(this)
)
}
return type === TriggerOpTypes.DELETE ? false : this
}
}

这里可以看到

  1. 通过工厂函数统一对涉及 写的函数进行覆盖
  2. 其他读函数如 size has get 则和正常代理逻辑一致

总结

readonly 是 vue-reactivity 中比较简单的一环,总结下来以下一些要点

  1. 设置 readonly 的原理是 proxy 代理 set 方法,不对 target 做处理,直接返回
  2. proxy 代理 get 区分 一般类型,和一些特殊的集合类型
    1. 如果是一般类型则对 value 递归 readonly 保证不可写
    2. 特殊set map 等集合类型,因为这些类型的写方法也在 get 操作里,所以需要对相应的方法进行代理覆盖,同时没有 set 相关代理逻辑