非原始值响应式方案(六)

只读和浅只读

我们希望一些数据是只读的,当用户尝试修改只读数据时,会收到一条警告信息。这样就实现了对数据的保护,例如组件接收到的 props 对象应该是一个只读数据。这是就要用到接下来我们要讨论的readonly函数,它能够将一个数据变成只读的:

1
2
3
const obj = readonly({ foo: 1 })

obj.foo = 2

只读本质上也是对数据对象的代理,我们同样可以使用createReactive函数来实现。如下面的代码所示,我们为 createReactive 函数增加第三个参数 isReadonly:

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
// 增加第三个参数isReadonly 代表是否只读,默认为false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截设置操作
set(target, key, newVal, receiver) {
if (isReadonly) {
console.warn(`属性 ${key}是只读的`)
return true
}
// 先获取旧值
const oldVal = target[key]
// type看看操作类型是添加新属性还是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
// 拿到执行结果
const res = Reflect.set(target, key, newVal, receiver)
// 如果两者相等,说明receiver是target的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key}是只读的`)
}
// 检查被操作的属性是否是对象自身的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)

if (res && hadKey) {
// 只有当被删除的属性是自身属性且成功被删除时,才触发更新
trigger(target, key, 'DELETE')
}
// 返回结果
return res
},
})
}

在这段代码中,当使用createReactive创建代理对象时,可以通过第三个参数isReadonly指定是否创建一个只读的代理对象。同时,我们还修改了 get 拦截函数和 deleteProperty 拦截函数的实现,因为对于一个对象来说,只读意味着既不可以设置对象的属性值,也不可以删除对象的属性。在这两个拦截函数中,我们分别添加了是否是只读的判断,一旦数据是只读的,则当这些操作发生时,会打印警告信息,提示用户这是一个非法操作。

当然,如果一个数据是只读的,那就意味着任何方式都无法修改它。因此,没有必要为只读数据建立响应联系。出于这个原因,当在副作用函数中读取一个只读属性的值时,不需要调用track函数追踪响应:

1
2
3
4
const obj = readonly({ foo: 1 })
effect(() => {
obj.foo // 可以读取值,但是不需要在副作用函数与数据之间建立响应联系
})

为了实现该功能,我们需要修改 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
// 增加第三个参数isReadonly 代表是否只读,默认为false,即非只读
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
// 如果访问的属性是raw我们把原始值返回
if (key === 'raw') {
return target
}
if (!isReadonly) {
// 追踪依赖
track(target, key)
}
// 得到原始值返回结果
const res = Reflect.get(target, key, receiver)

// 如果是浅响应,我们直接返回原始值
if (isShallow) {
return res
}

// 如果原始值结果还是一个对象,我们递归调用reactive方法对其进行一层包装使其变为相应数据
if (typeof res === 'object' && res !== null) {
// 调用reactive将结果包装成响应式数据并返回
return reactive(res)
}
// 返回res
return res
},
})
}

如上面的代码所示,在 get 拦截函数内检测 isReadonly 变量的值,判断是否是只读的,只有在非只读的情况下才会调用track函数建立响应联系。基于此,我们就可以实现readonly函数了:

1
2
3
function readonly(obj) {
return createReactive(obj, false, true)
}

然而,上面实现的readonly函数更应该叫作shallowReadonly,因为它没有做到深只读:

1
2
const obj = readonly({ foo: { bar: 1 } })
obj.foo.bar = 2

所以为了实现深只读,我们还应该在 get 拦截函数内递归地调用 readonly 将数据包装成只读的代理对象,并将其作为返回值返回:

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
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
// 如果访问的属性是raw我们把原始值返回
if (key === 'raw') {
return target
}
if (!isReadonly) {
// 追踪依赖
track(target, key)
}
// 得到原始值返回结果
const res = Reflect.get(target, key, receiver)

// 如果是浅响应,我们直接返回原始值
if (isShallow) {
return res
}

// 如果原始值结果还是一个对象,我们递归调用reactive方法对其进行一层包装使其变为相应数据
if (typeof res === 'object' && res !== null) {
// 如果数据为只读,则调用readonly对值进行包装并返回,否则将结果包装成响应式数据返回
return isReadonly ? readonly(res) : reactive(res)
}
// 返回res
return res
},
})
}

如上面的代码所示,我们在返回属性值之前,判断它是否是只读的,如果是只读的,则调用 readonly 函数对值进行包装,并把包装后的只读对象返回。
对于shallowReadonly,实际上我们只需要修改createReactive的第二个参数即可:

1
2
3
4
5
6
7
8
// 深只读
function readonly(obj) {
return createReactive(obj, false, true)
}
// 浅只读
function shallowReadonly(obj) {
return createReactive(obj, true, true)
}

如上面代码所示,在shallowReadonly函数内调用createReactive函数创建代理对象时,将第二个参数isShallow设置为 true,这样就可以创建一个浅只读的代理对象了。

代码

至此,我们实现了深只读和浅只读,贴一下最新的代码便于查看:

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
100
101
102
103
104
105
/**
* 创建响应式对象
* @param {*} obj 原始对象
* @param {*} isShallow 是否浅创建响应式对象
* @returns
*/
function createReactive(obj, isShallow = false, isReadonly = false) {
return new Proxy(obj, {
// 拦截读取操作
get(target, key, receiver) {
// 如果访问的属性是raw我们把原始值返回
if (key === 'raw') {
return target
}
// 非只读的时候才需要建立响应联系
if (!isReadonly) {
// 追踪依赖
track(target, key)
}
// 得到原始值返回结果
const res = Reflect.get(target, key, receiver)

// 如果是浅响应,我们直接返回原始值
if (isShallow) {
return res
}
// 追踪依赖
track(target, key)
// 如果原始值结果还是一个对象,我们递归调用reactive方法对其进行一层包装使其变为相应数据
if (typeof res === 'object' && res !== null) {
// 如果数据为只读,则调用readonly对值进行包装并返回,否则将结果包装成响应式数据返回
return isReadonly ? readonly(res) : reactive(res)
}
// 返回res
return res
},
// 拦截in操作符 'name' in p
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newVal, receiver) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key}是只读的`)
return true
}
// 先获取旧值
const oldVal = target[key]
// type看看操作类型是添加新属性还是设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET'
: 'ADD'
// 拿到执行结果
const res = Reflect.set(target, key, newVal, receiver)
// 如果两者相等,说明receiver是target的代理对象
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 如果是只读的,则打印警告信息并返回
if (isReadonly) {
console.warn(`属性 ${key}是只读的`)
return true
}
// 检查被操作的属性是否是对象自身的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)

if (res && hadKey) {
// 只有当被删除的属性是自身属性且成功被删除时,才触发更新
trigger(target, key, 'DELETE')
}
// 返回结果
return res
},
})
}
// 创建深响应
function reactive(obj) {
return createReactive(obj)
}

// 创建浅响应
function shallowReactive(obj) {
return createReactive(obj, true)
}

// 创建深只读
function readonly(obj) {
return createReactive(obj, false, true)
}
// 创建浅只读
function shallowReadonly(obj) {
return createReactive(obj, true, true)
}