各位靓仔靓女们,晚上好!我是你们今晚的Vue 3源码解读导游,外号“代码挖掘机”,今天咱们要一起深入Vue 3的“禁地”,探索readonly
和shallowReactive
这两个有趣的小家伙。
别担心,咱们不会迷路的,我会用最接地气的方式,带大家一层层揭开它们的神秘面纱。准备好了吗?系好安全带,发车咯!
一、开胃小菜:响应式系统的“味道”
在深入readonly
和shallowReactive
之前,咱们先简单回顾一下Vue 3响应式系统的“味道”。 Vue 3 的响应式系统,核心就是让数据变化驱动视图更新。 当你修改一个响应式对象的值时,Vue 3 就能自动追踪到这个变化,并通知相关的组件进行更新。
那么,问题来了,Vue 3 是怎么做到这一点的呢? 这就得归功于 Proxy
这个强大的武器了。 Vue 3 通过 Proxy
拦截对对象的访问和修改操作,并在这些操作发生时触发相应的依赖追踪和更新机制。
二、主角登场:readonly
闪亮登场
好了,开胃菜吃完了,现在咱们的主角之一——readonly
要闪亮登场了!
readonly
,顾名思义,就是“只读”的意思。 它可以将一个对象变成只读的,防止你意外地修改它。
想象一下,你有一个非常重要的数据,比如用户的个人信息,你不希望任何组件随意修改它,这时候 readonly
就能派上大用场了。
1. readonly
的用法:
import { readonly } from 'vue';
const original = { name: '张三', age: 30 };
const readOnlyObj = readonly(original);
console.log(readOnlyObj.name); // 输出:张三
// 尝试修改 readOnlyObj 的属性,会收到警告
// readOnlyObj.name = '李四'; // 报错:Set operation on key "name" failed: target is readonly.
看到了吗? 当你尝试修改 readonly
对象时,Vue 3 会发出警告,阻止你的操作。
2. readonly
的源码剖析:
让我们一起挖掘一下 readonly
的源码,看看它是如何实现只读功能的。
Vue 3 中 readonly
的核心实现, 也是基于 Proxy
。 它创建了一个新的 Proxy
对象,并拦截了 set
操作。 当你尝试设置 Proxy
对象的属性时,set
拦截器会被触发,并阻止你的操作。
// Packages/reactivity/src/readonly.ts
import { mutableToReadonly } from './reactive';
import { isObject, toRaw } from '@vue/shared';
import {
mutableHandlers,
readonlyHandlers,
shallowReactiveHandlers,
shallowReadonlyHandlers
} from './baseHandlers';
import {
ReactiveFlags,
reactive,
isReadonly,
isReactive,
targetTypeMap,
TargetType
} from './reactive';
export const readonly = <T extends object>(
target: T
): Readonly<T> => {
return createReadonlyObject(
target,
false,
readonlyHandlers,
mutableToReadonly
)
}
function createReadonlyObject(
target: any,
isShallow: boolean,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<any, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a ReactiveProxy we can return the same one.
if (
target[ReactiveFlags.RAW] &&
!(isShallow && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target is already a readonly proxy, return that.
if (proxyMap.has(target)) {
return proxyMap.get(target)
}
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(
target,
baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
export const enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2
}
function targetTypeValidator(raw: object, key: string | symbol) {
return isObject(raw)
? String(key) === '__v_isRef' || key === ReactiveFlags.RAW
? TargetType.COMMON
: (Object.hasOwn(raw, key) ? TargetType.COMMON : TargetType.INVALID)
: TargetType.INVALID
}
// Packages/reactivity/src/baseHandlers.ts
import { isObject, toRaw, hasChanged } from '@vue/shared'
import { track, trigger } from './effect'
import { ReactiveFlags, reactive, toReactive, toReadonly } from './reactive'
import { isRef, Ref } from '../../packages/reactivity/src/ref'
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true, true)
function createGetter(isReadonly: boolean = false, shallow: boolean = false) {
return function get(target: object, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW &&
receiver === reactiveMap.get(target)
) {
return target
}
const targetIsArray = Array.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) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, 'get', key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - only for reactive deps
return targetIsArray && isIntegerKey(key)
? res
: res.value
}
if (isObject(res)) {
// Convert returned value into reactive/readonly object.
return isReadonly ? toReadonly(res) : toReactive(res)
}
return res
}
}
const set = createSetter()
function createSetter() {
return function set(
target: object,
key: string | symbol,
value: any,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (!hasChanged(oldValue, value)) {
return true
}
if (isReadonly(target)) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {
const oldValue = (target as any)[key]
if (hasChanged(value, oldValue)) {
trigger(target, 'set', key, value, oldValue)
}
}
return result
}
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target: object, key: string | symbol, value: any, receiver: object): boolean {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target: object, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
}
在 readonlyHandlers
对象中, set
方法直接返回 true
,并且在开发环境下还会发出警告。 这样就阻止了对 readonly
对象的修改。
3. 深度 readonly
:
readonly
默认是深度的,也就是说,如果你的对象中包含嵌套的对象,那么所有嵌套的对象都会被变成只读的。
import { readonly } from 'vue';
const original = {
name: '张三',
address: {
city: '北京',
street: '长安街'
}
};
const readOnlyObj = readonly(original);
// 尝试修改嵌套对象的属性,会收到警告
// readOnlyObj.address.city = '上海'; // 报错:Set operation on key "city" failed: target is readonly.
三、另一位主角:shallowReactive
登场
接下来,让我们欢迎另一位主角——shallowReactive
!
shallowReactive
, 顾名思义,就是“浅层响应式”的意思。 它只会将对象的第一层属性变成响应式的,而不会递归地处理嵌套对象。
1. shallowReactive
的用法:
import { shallowReactive } from 'vue';
const original = {
name: '张三',
address: {
city: '北京',
street: '长安街'
}
};
const shallowReactiveObj = shallowReactive(original);
// 修改 shallowReactiveObj 的 name 属性,会触发更新
shallowReactiveObj.name = '李四'; // 触发更新
// 修改 shallowReactiveObj.address.city 属性,不会触发更新
shallowReactiveObj.address.city = '上海'; // 不触发更新
看到了吗? 当你修改 shallowReactive
对象的 name
属性时,Vue 3 会触发更新。 但是,当你修改嵌套对象 address
的 city
属性时,Vue 3 并不会触发更新。
2. shallowReactive
的源码剖析:
shallowReactive
的源码实现,也基于 Proxy
。 它创建了一个新的 Proxy
对象,并拦截了 get
和 set
操作。 但是,与 reactive
不同的是, shallowReactive
在 get
拦截器中不会递归地将嵌套对象变成响应式的。
// Packages/reactivity/src/reactive.ts
import {
mutableHandlers,
readonlyHandlers,
shallowReactiveHandlers,
shallowReadonlyHandlers
} from './baseHandlers';
import {
isObject,
toRaw,
hasOwn,
isSymbol,
isArray,
isMap,
isSet
} from '@vue/shared';
export const reactive = <T extends object>(target: T): T => {
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableToProxy
)
}
export const shallowReactive = <T extends object>(target: T): T => {
return createReactiveObject(
target,
true,
shallowReactiveHandlers,
shallowToProxy
)
}
function createReactiveObject(
target: any,
isShallow: boolean,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<any, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a ReactiveProxy we can return the same one.
if (
target[ReactiveFlags.RAW] &&
!(isShallow && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target is already a proxy, return that.
if (proxyMap.has(target)) {
return proxyMap.get(target)
}
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(
target,
baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
// Packages/reactivity/src/baseHandlers.ts
// 省略很多代码
function createGetter(isReadonly: boolean = false, shallow: boolean = false) {
return function get(target: object, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW &&
receiver === reactiveMap.get(target)
) {
return target
}
const targetIsArray = Array.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) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, 'get', key)
}
if (shallow) {
return res // 关键所在:shallow 为 true 时,直接返回 res,不进行深层递归
}
if (isRef(res)) {
// ref unwrapping - only for reactive deps
return targetIsArray && isIntegerKey(key)
? res
: res.value
}
if (isObject(res)) {
// Convert returned value into reactive/readonly object.
return isReadonly ? toReadonly(res) : toReactive(res)
}
return res
}
}
在 createGetter
函数中,当 shallow
为 true
时, get
拦截器会直接返回属性值 res
,而不会调用 toReactive
或 toReadonly
将其转换成响应式对象。 这就是 shallowReactive
实现浅层响应式的关键。
3. shallowReactive
的适用场景:
shallowReactive
在某些特定的场景下非常有用。 比如,当你的对象中包含大量的数据,而你只需要监听第一层属性的变化时,使用 shallowReactive
可以提高性能。
另一个常见的场景是,当你的对象中包含一些不受 Vue 3 控制的外部对象时,比如第三方库的对象,使用 shallowReactive
可以避免 Vue 3 尝试劫持这些对象,从而避免一些潜在的问题。
四、shallowReadonly
:只读且浅层
既然有 readonly
和 shallowReactive
,那有没有 shallowReadonly
呢? 答案是肯定的!
shallowReadonly
结合了 readonly
和 shallowReactive
的特点,它会创建一个只读的、浅层响应式对象。 也就是说,你不能修改 shallowReadonly
对象的任何属性,但是它只会阻止你修改第一层属性,而不会递归地阻止你修改嵌套对象的属性。
1. shallowReadonly
的用法:
import { shallowReadonly } from 'vue';
const original = {
name: '张三',
address: {
city: '北京',
street: '长安街'
}
};
const shallowReadonlyObj = shallowReadonly(original);
// 尝试修改 shallowReadonlyObj 的 name 属性,会收到警告
// shallowReadonlyObj.name = '李四'; // 报错:Set operation on key "name" failed: target is readonly.
// 尝试修改 shallowReadonlyObj.address.city 属性,不会收到警告,但也不会触发更新
shallowReadonlyObj.address.city = '上海'; // 不报错,不触发更新
2. shallowReadonly
的源码剖析:
shallowReadonly
的源码实现与 readonly
和 shallowReactive
类似, 也是基于 Proxy
。它结合了 readonlyHandlers
和浅层 reactive 的 get 拦截器。
// Packages/reactivity/src/reactive.ts
export const shallowReadonly = <T extends object>(
target: T
): Readonly<T> => {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyToProxy
)
}
// Packages/reactivity/src/baseHandlers.ts
export const shallowReadonlyHandlers: ProxyHandler<object> = {
get: shallowReadonlyGet,
set(target: object, key: string | symbol, value: any, receiver: object): boolean {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
},
deleteProperty(target: object, key: string | symbol): boolean {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
}
五、总结与对比:
为了帮助大家更好地理解 readonly
、shallowReactive
和 shallowReadonly
的区别,我特意准备了一张表格:
特性 | reactive |
readonly |
shallowReactive |
shallowReadonly |
---|---|---|---|---|
响应式 | 深层 | 深层 | 浅层 | 浅层 |
只读 | 否 | 是 | 否 | 是 |
修改第一层属性 | 触发更新 | 报错 | 触发更新 | 报错 |
修改嵌套属性 | 触发更新 | 报错 | 不触发更新 | 不报错,不触发更新 |
六、实战演练:
说了这么多理论,不如来点实际的。 让我们通过一个简单的例子,来演示一下 readonly
和 shallowReactive
的用法。
假设我们有一个组件,需要显示用户的个人信息。 但是,我们不希望该组件能够修改用户的个人信息。 这时候,我们就可以使用 readonly
来保护用户的数据。
<template>
<div>
<p>姓名:{{ userInfo.name }}</p>
<p>年龄:{{ userInfo.age }}</p>
<button @click="updateName">修改姓名(无效)</button>
</div>
</template>
<script>
import { ref, readonly, onMounted } from 'vue';
export default {
setup() {
const userInfo = ref({ name: '张三', age: 30 });
const readOnlyUserInfo = readonly(userInfo);
const updateName = () => {
// 尝试修改 readOnlyUserInfo 的 name 属性,会收到警告
readOnlyUserInfo.value.name = '李四'; // 报错:Set operation on key "name" failed: target is readonly.
};
onMounted(() => {
//即使直接修改userInfo.value,因为已经通过readonly包装,仍然无法修改
userInfo.value.name = '王五'
})
return {
userInfo: readOnlyUserInfo,
updateName
};
}
};
</script>
在这个例子中,我们使用 readonly
将 userInfo
对象变成只读的。 这样,即使我们在组件中尝试修改 userInfo
的 name
属性,也会收到警告,从而保证了用户数据的安全性。
再来一个 shallowReactive
的例子:
假设我们有一个组件,需要显示一个复杂的对象,该对象包含很多嵌套的属性。 但是,我们只需要监听该对象的第一层属性的变化。 这时候,我们就可以使用 shallowReactive
来提高性能。
<template>
<div>
<p>姓名:{{ person.name }}</p>
<p>城市:{{ person.address.city }}</p>
<button @click="updateName">修改姓名</button>
<button @click="updateCity">修改城市(无效)</button>
</div>
</template>
<script>
import { ref, shallowReactive } from 'vue';
export default {
setup() {
const person = shallowReactive({
name: '张三',
address: {
city: '北京',
street: '长安街'
}
});
const updateName = () => {
person.name = '李四'; // 触发更新
};
const updateCity = () => {
person.address.city = '上海'; // 不触发更新
};
return {
person,
updateName,
updateCity
};
}
};
</script>
在这个例子中,我们使用 shallowReactive
将 person
对象变成浅层响应式的。 这样,当我们修改 person
的 name
属性时,Vue 3 会触发更新。 但是,当我们修改 person.address.city
属性时,Vue 3 并不会触发更新。
七、总结:
好了,今天的源码解读就到这里了。 相信通过今天的学习,大家对 readonly
和 shallowReactive
已经有了更深入的了解。 记住,它们都是 Vue 3 响应式系统中非常有用的工具,可以帮助我们更好地管理数据,提高性能。
希望今天的讲座对大家有所帮助,咱们下次再见! 拜拜!