Vue 3 源码探秘:reactive
对象的 isShallow
和 isReadonly
之谜
哈喽大家好!今天咱们来聊聊 Vue 3 响应式系统里两个挺重要的标志位:__v_isShallow
和 __v_isReadonly
。 别看它们名字有点长,其实作用可大了,直接影响着你的数据能不能被深度追踪,能不能被修改。 我们就从 reactive
函数开始,一路追踪到 Proxy 的处理,彻底搞清楚这两个标志位是怎么控制 Proxy
行为的。
1. reactive
函数:响应式世界的入口
首先,让我们回顾一下 reactive
函数,它是创建响应式对象的关键。
// packages/reactivity/src/reactive.ts
import {
mutableHandlers,
readonlyHandlers,
shallowReactiveHandlers,
shallowReadonlyHandlers
} from './baseHandlers'
import { isObject, toRaw } from '@vue/shared'
import {
mutableCollectionHandlers,
readonlyCollectionHandlers,
shallowCollectionHandlers,
shallowReadonlyCollectionHandlers
} from './collectionHandlers'
export const reactiveMap = new WeakMap<object, any>()
export const readonlyMap = new WeakMap<object, any>()
export function reactive<T extends object>(target: T): T {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
export function readonly<T extends object>(target: T): T {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}
export function shallowReactive<T extends object>(target: T): T {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
reactiveMap
)
}
export function shallowReadonly<T extends object>(target: T): T {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowCollectionHandlers,
readonlyMap
)
}
function createReactiveObject(
target: object,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
if (!isObject(target)) {
return target
}
// target is already a Reactive object, return existing proxy.
// also avoid circular recursion.
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target is already a Readonly object, return existing proxy.
if (
proxyMap.has(target)
) {
return proxyMap.get(target)
}
// only a plain object can be made reactive.
if (!canObserve(target)) {
return target
}
const proxy = new Proxy(
target,
isCollectionType(target) ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
export const enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
RAW = '__v_raw',
IS_SHALLOW = '__v_isShallow',
IS_REF = '__v_isRef'
}
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value as object) : value
export const toReadonly = <T extends unknown>(value: T): T =>
isObject(value) ? readonly(value as object) : value
export function isReactive(value: unknown): boolean {
if (!isObject(value)) {
return false
}
return !!(value as any)[ReactiveFlags.IS_REACTIVE]
}
export function isReadonly(value: unknown): boolean {
return !!(value as any)[ReactiveFlags.IS_READONLY]
}
export function isShallow(value: unknown): boolean {
return !!(value as any)[ReactiveFlags.IS_SHALLOW]
}
export function isProxy(value: unknown): boolean {
return isReactive(value) || isReadonly(value)
}
export const toRaw = <T>(observed: T): T => {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
export const markRaw = <T extends object>(value: T): T => {
def(value, ReactiveFlags.SKIP, true)
return value
}
const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string => objectToString.call(value)
function isCollectionType(target: any): boolean {
return (
toTypeString(target) === '[object Map]' ||
toTypeString(target) === '[object Set]' ||
toTypeString(target) === '[object WeakMap]' ||
toTypeString(target) === '[object WeakSet]'
)
}
const canObserve = (value: any): boolean => {
return (
!value[ReactiveFlags.SKIP] &&
!Object.isFrozen(value) &&
!isNative(value)
)
}
// implementation
function def(obj: object, key: string | symbol, value: any) {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}
const isNative = (value: any): boolean => {
return isObject(value) && typeof value.__v_skip === 'boolean'
}
reactive
函数的核心是调用 createReactiveObject
函数。 我们重点关注 createReactiveObject
函数的参数:
target
: 需要变成响应式的目标对象。isReadonly
: 一个布尔值,决定创建的是只读还是可变的响应式对象。baseHandlers
: Proxy 的 handlers 对象,定义了 get, set, deleteProperty 等拦截行为。collectionHandlers
: 专门用于处理 Map, Set, WeakMap, WeakSet 等集合类型的 handlers。proxyMap
: 使用WeakMap
来存储已经创建过的 proxy 对象,避免重复代理。
在 createReactiveObject
内部,会检查目标对象是否已经是一个 reactive 或者 readonly 对象。 如果是,直接返回已有的 proxy,避免重复创建。 如果不是,就创建一个 Proxy
实例,并把 target
和对应的 handlers
传递给它。
2. Proxy Handler:拦截行为的定义者
reactive
和 readonly
的核心行为都体现在它们使用的 Proxy
的 handler 上。 Vue 3
为了提高性能,将 handler
进行了拆分和复用。 主要分为了以下几类:
mutableHandlers
: 用于reactive
创建的可变对象。readonlyHandlers
: 用于readonly
创建的只读对象。shallowReactiveHandlers
: 用于shallowReactive
创建的浅层可变对象。shallowReadonlyHandlers
: 用于shallowReadonly
创建的浅层只读对象。mutableCollectionHandlers
,readonlyCollectionHandlers
,shallowCollectionHandlers
,shallowReadonlyCollectionHandlers
: 用于集合类型的handler
。
我们以 mutableHandlers
为例,看看它的定义:
// packages/reactivity/src/baseHandlers.ts
import { track, trigger } from './effect'
import { ReactiveFlags, reactive, readonly, toRaw, isObject } from './reactive'
import { isRef } from './ref'
import { TrackOpTypes, TriggerOpTypes } from './operations'
const get = /*#__PURE__*/ createGetter()
const set = /*#__PURE__*/ createSetter()
const deleteProperty = /*#__PURE__*/ createDeleteProperty()
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true)
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has: (target, key) => {
return key in target
},
ownKeys: (target) => {
return Reflect.ownKeys(target)
}
}
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
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
},
has: (target, key) => {
return key in target
},
ownKeys: (target) => {
return Reflect.ownKeys(target)
}
}
export const shallowReactiveHandlers: ProxyHandler<object> = {
get: shallowReactiveGet,
set,
deleteProperty,
has: (target, key) => {
return key in target
},
ownKeys: (target) => {
return Reflect.ownKeys(target)
}
}
export const shallowReadonlyHandlers: ProxyHandler<object> = {
get: shallowReadonlyGet,
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
},
has: (target, key) => {
return key in target
},
ownKeys: (target) => {
return Reflect.ownKeys(target)
}
}
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.IS_SHALLOW) {
return shallow
} 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 (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
const set = /*#__PURE__*/ createSetter()
function createSetter() {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (!isReadonly(target)) {
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
if (hadKey) {
if (value !== oldValue) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
} else {
trigger(target, TriggerOpTypes.ADD, key, value)
}
return result
} else {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
}
}
function createDeleteProperty() {
return function deleteProperty(target: object, key: string | symbol): boolean {
if (!isReadonly(target)) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, undefined)
}
return result
} else {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
}
}
}
function hasOwn(val: object, key: string | symbol): key is keyof typeof val {
return Object.prototype.hasOwnProperty.call(val, key)
}
const arrayInstrumentations: Record<string, Function> = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach((key) => {
const method = Array.prototype[key]
arrayInstrumentations[key] = function (...args: any[]) {
const arr = toRaw(this)
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we need to check if the argument is also a reactive(nested) object
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// not found
return method.apply(this, args)
} else {
return res
}
}
})
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key) => {
const method = Array.prototype[key]
arrayInstrumentations[key] = function (...args: any[]) {
// we need to disable tracking inside array methods to avoid duplicate
// triggers. See issue #q186
pauseTracking()
const res = method.apply(this, args)
resetTracking()
return res
}
})
function pauseTracking() {
throw new Error('Function not implemented.')
}
function resetTracking() {
throw new Error('Function not implemented.')
}
function isIntegerKey(key: any) {
return typeof key === 'string' &&
key === String(Number(key)) &&
Number.isInteger(Number(key))
}
mutableHandlers
定义了可变对象的 get
, set
, deleteProperty
等行为。 readonlyHandlers
则禁止 set
和 deleteProperty
操作,并且在开发环境下会发出警告。
3. __v_isShallow
和 __v_isReadonly
:标志位的诞生与意义
现在,我们终于要聚焦到 __v_isShallow
和 __v_isReadonly
这两个标志位了。 它们是在 createGetter
函数中被设置的:
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.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver === reactiveMap.get(target)
) {
return target
}
// ... 省略其他代码
}
}
可以看到,createGetter
接收两个参数:
isReadonly
: 决定是否是只读的。shallow
: 决定是否是浅层的。
在 get
函数内部,当访问 ReactiveFlags.IS_REACTIVE
、ReactiveFlags.IS_READONLY
或 ReactiveFlags.IS_SHALLOW
属性时,会返回对应的布尔值。 这些布尔值就存储在 __v_isReactive
、__v_isReadonly
和 __v_isShallow
属性中。
那么,这些标志位有什么用呢?
__v_isReadonly
: 这个标志位用于标识对象是否是只读的。 如果一个对象被标记为只读,那么任何尝试修改它的属性都会被拦截,并且在开发环境下会发出警告。__v_isShallow
: 这个标志位用于标识对象是否是浅层的。 如果一个对象被标记为浅层,那么只有对象的第一层属性会被追踪。 深层嵌套的对象不会被转换成响应式对象。
4. __v_isShallow
如何控制 Proxy 的行为?
__v_isShallow
标志位的关键作用在于控制 reactive
函数是否对嵌套对象进行递归处理。
在 createGetter
函数中,可以看到这段代码:
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
这段代码的意思是,如果当前访问的属性值是一个对象,那么会递归调用 reactive
或 readonly
函数,将其也转换成响应式对象。 但是,如果 shallow
为 true
,也就是创建的是 shallowReactive
或 shallowReadonly
对象,那么这段代码就不会执行。
例子:
const obj = {
a: 1,
b: {
c: 2
}
}
const reactiveObj = reactive(obj);
const shallowReactiveObj = shallowReactive(obj);
console.log(isReactive(reactiveObj.b)); // true,因为 reactive 会递归处理
console.log(isReactive(shallowReactiveObj.b)); // false,因为 shallowReactive 不会递归处理
reactiveObj.b.c = 3; // 触发响应式更新
shallowReactiveObj.b.c = 4; // 不触发响应式更新
在这个例子中,reactiveObj
是一个深度响应式对象,所以修改 reactiveObj.b.c
会触发响应式更新。 而 shallowReactiveObj
是一个浅层响应式对象,所以修改 shallowReactiveObj.b.c
不会触发响应式更新。
5. __v_isReadonly
如何控制 Proxy 的行为?
__v_isReadonly
标志位的作用是禁止对对象的修改。
在 readonlyHandlers
和 shallowReadonlyHandlers
中,set
和 deleteProperty
拦截器会阻止任何修改操作,并且在开发环境下会发出警告。
例子:
const obj = {
a: 1
}
const readonlyObj = readonly(obj);
readonlyObj.a = 2; // 报错:Set operation on key "a" failed: target is readonly.
delete readonlyObj.a; // 报错:Delete operation on key "a" failed: target is readonly.
6. 总结
标志位 | 作用 | 如何控制 Proxy 行为 |
---|---|---|
__v_isShallow |
标识对象是否是浅层的。 浅层响应式对象只追踪第一层属性,深层嵌套的对象不会被转换成响应式对象。 | 在 createGetter 中,如果 shallow 为 true ,则不会递归调用 reactive 或 readonly 函数,因此深层嵌套的对象不会被代理。 |
__v_isReadonly |
标识对象是否是只读的。 只读对象不允许修改属性。 | 在 readonlyHandlers 和 shallowReadonlyHandlers 中,set 和 deleteProperty 拦截器会阻止任何修改操作,并且在开发环境下会发出警告。 |
总而言之,__v_isShallow
和 __v_isReadonly
就像两个开关,控制着 reactive
对象行为。 __v_isShallow
控制了响应式追踪的深度,__v_isReadonly
控制了对象的可变性。 掌握了这两个标志位的含义,可以更好地理解 Vue 3 的响应式系统,也能在实际开发中更加灵活地运用它们。
好了,今天的分享就到这里。 希望大家有所收获!下次有机会再一起深入 Vue 3 的源码!