「Vue3学习篇」-readonly()、isReadonly() 、shallowReadonly()
『引言』
本篇介绍readonly()、isReadonly()和shallowReadonly()这三个API。关于这三个API是用来干什么的,以及使用方法等等都会有说明。
『readonly()』
『定义』
【官方解释】
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
-
详细信息
只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与
reactive()
相同,但解包得到的值是只读的。
【我的理解】
readonly()可以接受『响应式对象、纯对象、ref』
,返回原始对象的只读代理。
- 这个只读代理是深层的,任何被访问的嵌套属性也是只读的。与reactive()一样,如果任何属性使用了ref,当它通过代理访问时,则被自动解包。
『用法』
语法: readonly()
『官网示例🌰』
const original = reactive({ count: 0 })
// 常量copy是只读的,original值发生变化时,copy也会变化。
const copy = readonly(original)
watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count)
})
// 更改源属性会触发其依赖的侦听器
original.count++
// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning!
『注意⚠️』
- 使用
readonly()
将数据变为只读的数据后,数据就不可以修改
了。 - 如果要是强制修改的话,控制台会
报警告信息
。 - 使用
readonly()
可以避免错误的操作
,增加代码的安全性和可维护性
。
『示例🌰』
<template>
<div>
<h1>人物简介</h1>
<p>姓名:{{info.name}}</p>
<p>年龄:{{info.age}}岁</p>
<p>爱好:{{info.hobbies.join('、')}}</p>
<p>地址:{{info.address.provice}} - {{ info.address.city }} </p>
<p>描述:{{description}}</p>
<button @click="Modifyname">
修改姓名
</button>
<button @click="Modifydescription">
修改描述
</button>
</div>
</template>
<script setup>
import { ref, reactive, readonly } from 'vue'
const info = reactive({
name: 'pupu',
age: 10,
hobbies: ['唱歌', '画画'],
address: {
provice: '浙江省',
city: '杭州市'
}
})
const description = ref('一点也不可爱,不喜欢吃蜂蜜!')
const myReadonlyInfo = readonly(info)
const myReadonlydescription = readonly(description)
const Modifyname = () => {
myReadonlyInfo.name = 'wnxx'
}
const Modifydescription = () => {
myReadonlydescription.value = '非常的可爱,特别喜欢吃蜂蜜'
}
</script>
『效果展示』
『代码解析』
上面示例中,使用reactive
定义了一个info对象
,使用ref
定义了一个description
,然后使用readonly()
方法返回一个只读对象。
当点击修改姓名和修改描述按钮🔘时,会发现控制台会报出警告⚠️信息:[Vue warn] Set operation on key "xxx" failed: target is readonly.
与此同时,视图也并未发生改变,姓名和描述都无法修改
。
『readonly源码』
这个就是readonly源码
,可以看到内部调用createReactiveObject函数
。
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}
『参数说明』:
- 第一个参数target。
- 第二个参数是isReadonly,为true。
- 第三个参数readonlyHandlers是readonly对应的处理函数。
- 第四个参数是对于集合类型的对象进行处理的readonly所对应的函数。
再来看一下createReactiveObject函数
源码。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
// 获取target的类型,如果其为INVALID,说明不能进行代理,所以直接返回target
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 根据target生成对应的proxy对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
//将代理对象缓存在对应proxyMap中
proxyMap.set(target, proxy)
return proxy
}
createReactiveObject
主要是判断target的类型,然后通过判断看是直接返回target
,还是返回一个新建的proxy
。
createReactiveObject
方法的最后一个参数proxyMap
为readonly
创建的weakMap
用来缓存代理对象。
createReactiveObject
根据普通对象和数组类型、Set和Map类型来区分baseHandlers
和collectionHandlers
。
- baseHandlers处理数组,对象类型。
- collectionHandlers处理Map、Set、WeakMap类型。
🙋🙋♂️提问🚩:
什么情况下会直接返回🔙target,那又是什么情况下返回🔙一个新建的proxy❓🤔🤔
回答📒:
直接返回🔙target的情况:
- target不是对象。
- target已经是个proxy对象
- target类型校验不合法。
返回🔙一个新建的proxy的情况:
当参数proxyMap
对应的实参里已经存在了target的响应式对象时,直接取出并返回该响应式对象。
否则,创建一个target的响应式对象proxy
,将proxy
加入到proxyMap
中,然后返回该proxy
。
createReactiveObject函数小结
如果不是对象,即不是Object,Array,Map,Set,WeakMap,WeakSet类型的,就不可以代理,在开发模式抛出警告,生产环境直接返回目标对象。
已经是响应式的就直接返回(取ReactiveFlags.RAW 属性会返回true,因为过程中会用weakMap
进行保存,通过target能判断出是否有ReactiveFlags.RAW属性)。
例外:对reactive对象进行readonly()。
如果target已经存在对应的代理对象,则直接从WeakMap
数据结构中取出这个Proxy
对象(对应的代理对象)并返回。
只对targetTypeMap
类型白名单中的类型进行响应式处理。
『isReadonly()』
『定义』
【官方解释】
检查传入的值是否为只读对象。只读对象的属性可以更改,但他们不能通过传入的对象直接赋值。
通过 readonly()
和 shallowReadonly()
创建的代理都是只读的,因为他们是没有 set
函数的 computed()
ref。
【我的理解】
isReadonly()用于判断对象是否是由readonly()
、shallowReadonly()
创建的只读代理。
『用法』
语法:isReadonly()
只有readonly
、shallowReadonly
建立的代理才会返回true,其他的都是false。
『示例🌰』
<template>
<div>
<h1>人物简介</h1>
<p>姓名:{{info.name}}</p>
<p>年龄:{{info.age}}岁</p>
<p>爱好:{{info.hobbies.join('、')}}</p>
<p>地址:{{address.provice}} - {{ address.city }} </p>
<p>描述:{{description}}</p>
<button @click="isJudgeIsReadonly">
isReadonly判断
</button>
</div>
</template>
<script setup>
import { ref, readonly, shallowReadonly, isReadonly } from 'vue'
const info = readonly({
name: 'pupu',
age: 10,
hobbies: ['唱歌', '画画'],
})
const address = shallowReadonly({
provice: '浙江省',
city: '杭州市'
})
const description = ref('一点也不可爱,不喜欢吃蜂蜜!')
const isJudgeIsReadonly = () => {
console.log('判断info', isReadonly(info))
console.log('判断address', isReadonly(address))
console.log('判断description', isReadonly(description))
}
</script>
『效果展示』
『代码解析』
上面代码中,使用readonly包裹了一个info,使用shallowReadonly包裹了一个address,最后使用ref定义了description。
通过isReadonly()来判断,可以在控制台打印的信息中看到前两者返回true,后者返回的是false。
『isReadonly源码』
isReadonly源码如下⬇️:
export function isReadonly(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
- ReactiveFlags.IS_READONLY是一个字符串,值为:
__v_isReadonly
。 - 判断当前对象的
__v_isReadonly
属性是否是true,并返回。
『shallowReadonly()』
『定义』
【官方解释】
readonly()
的浅层作用形式。
-
详细信息
和
readonly()
不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。‼️谨慎使用
浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。
【我的理解】
shallowReadonly()用于创建一个proxy,自身的属性为只读,但深层数据不受影响。
『用法』
语法:shallowReadonly()
『官网示例🌰』
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性会失败
state.foo++
// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false
// 这是可以通过的
state.nested.bar++
『注意』
- 只有
第一层数据变成只读
,不可修改,深层数据可修改
。 - 修改第一层数据,控制台会
报错警告信息
。
『示例🌰』
<template>
<div>
<h1>人物简介</h1>
<p>姓名:{{info.name}}</p>
<p>年龄:{{info.age}}岁</p>
<p>爱好:{{info.hobbies.join('、')}}</p>
<p>地址:{{info.address.provice}} - {{ info.address.city }} </p>
<p>描述:{{info.description}}</p>
<button @click="ModifyInfo">
shallowReadonly修改
</button>
</div>
</template>
<script setup>
import { reactive, shallowReadonly } from 'vue'
const info = reactive({
name: 'wnxx',
age: 3,
hobbies: ['打羽毛球', '旅游'],
address:{
provice: '浙江省',
city: '杭州市'
},
description: '非常的可爱,特别喜欢吃蜂蜜!'
})
const newInfo = shallowReadonly(info)
const ModifyInfo = () => {
newInfo.name = 'pupu'
newInfo.age = 10
newInfo.hobbies = ['唱歌', '画画']
newInfo.address.provice = '云南省'
newInfo.address.city = '丽江市'
newInfo.description = '一点也不可爱,不喜欢吃蜂蜜!'
console.log(newInfo, 'newInfo')
}
</script>
『效果展示』
『代码解析』
可以看到修改newInfo.name、newInfo.age等第一层数据时,控制台会报出警告信息:[Vue warn] Set operation on key "XXX" failed: target is readonly.
。
当修改newInfo.address.provice和newInfo.address.city深层数据时,视图会改变,数据也可以修改。
『shallowReadonly源码』
shallowReadonly源码如下⬇️:
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
shallowReadonly源码中,在入口函数中,有几个参数。
- 第一个参数还是target;
- 第二个是isReadonly,为true;
- 第三个参数shallowReadonlyHandlers是shallowReadonly对应的处理函数;
- 第四个参数是对于集合类型的对象进行处理的shallowReadonly所对应的函数。
然后就是与reactive和readonly一样,都是调用的createReactiveObject函数。
转载自:https://juejin.cn/post/7269045227087527970