
在开始 readonly 之前,我们先补充一下 Proxy 的知识:
Proxy
Proxy
是实现 reactive
、readonly
等功能的核心。它会在目标对象前架设一个"代理"或"拦截层",让我们有机会对外界的访问操作进行自定义处理。
拦截与代理
Proxy
的工作模式可以想象成一个保安:
- 目标对象 (
target
) :是公司内部的办公室。 - 代理对象 (
proxy
) :保安本人。 - 处理器 (
handler
) :是保安的应对手册,里面写了访问对象时该如何处理的逻辑。
任何外部代码(访客)要访问对象属性(进办公室)都需要经过 Proxy
(保安),Proxy
会查询 handler
(保安手册)来决定如何响应。
在 handler
中,最关键的陷阱 (trap) 之一就是 get
。get(target, key, receiver)
:这个陷阱的触发时机是当代码试图读取代理对象属性时。即使原始对象上并不存在这个属性,它也可以通过 handler 的规则去处理。
了解这些之后,可以开始实现了!
readonly
只接受对象参数。在前面的文章中我们提到,ref
如果传入的是对象,那它内部也会调用 reactive
。因此,在 readonly
的实现中,我们只要能正确处理 reactive
对象(或普通对象)就可以。
HTML
<body>
<div id="app"></div>
<script type="module">
import { readonly, reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { readonly, effect, reactive } from '../dist/reactivity.esm.js'
const state = reactive({
a: 1,
b: {
c: 1
}
})
const readonlyState = readonly(state)
effect(() => {
console.log(readonlyState.a)
})
setTimeout(() => {
state.a++ // 修改原始的 reactive 对象
}, 1000)
</script>
</body>

如果你设置一个 readonly
对象,当修改原始的 reactive
对象时,readonly
仍然会接收到响应式的触发更新。
JavaScript
setTimeout(() => {
readonlyState.a++ // 尝试修改 readonly 对象
}, 1000)
但如果你修改的是
readonly
对象本身,那就会在控制台收到警告。
查看这个
readonly
对象,可以发现它很像一个 reactive
对象,是通过 _isReadonly
标记来判断的。这跟我们上一个章节在实现 shallow
时的思路特别像。
首先,我们先在 ref.ts
中增加枚举标记,分别是 IS_REACTIVE
以及 IS_READONLY
:
TypeScript
// ref.ts
export enum ReactiveFlags {
IS_REF = '__v_isRef',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly'
}
接着调整一下 reactive.ts
,我们移除原有的 Set
检查,改为通过标记来判断是否需要重复代理。
TypeScript
// reactive.ts
import { ReactiveFlags } from './ref'
// ...
function createReactiveObject(target, handlers, proxyMap) {
// reactive 只处理对象
if (!isObject(target)) return target
// 统一处理"防止重复代理"的情况
// 如果 target 已经是 reactive 或 readonly,直接返回
if (target[ReactiveFlags.IS_REACTIVE] || target[ReactiveFlags.IS_READONLY]) {
return target
}
// 如果这个 target 已经被代理过,直接返回已经创建好的 proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 创建 target 的代理对象
const proxy = new Proxy(target, handlers)
// 存储 target 和响应式对象的关联关系
proxyMap.set(target, proxy)
return proxy
}
// ...
// 调整 isReactive 判断
export function isReactive(target) {
return !!(target && target[ReactiveFlags.IS_REACTIVE])
}
// 先新增一个空实现,等一下再来补充
export function readonly(target) {
return createReactiveObject(target, readonlyHandlers, readonlyMap) // (提前写好)
}
// 新增 readonly 判断
export function isReadonly(value) {
return !!(value && value[ReactiveFlags.IS_READONLY])
}
接着回到 baseHandlers.ts
,新增一个 readonlyHandler
。
TypeScript
// baseHandlers.ts
// 导入标记
import { isRef, ReactiveFlags } from './ref'
// 引入 reactive 和 readonly 函数
import { reactive, readonly } from './reactive'
// 扩展 createGetter,使其接受一个 isReadonly 参数
function createGetter(isShallow = false, isReadonly = false) {
return function get(target, key, receiver) {
// 拦截对标记的访问
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
// 非只读时才收集依赖
if (!isReadonly) {
track(target, key)
}
const res = Reflect.get(target, key, receiver)
if (isRef(res)) {
return res.value
}
if (isObject(res)) {
// 关键:如果是只读,则递归调用 readonly
return isReadonly ? readonly(res) : (isShallow ? res : reactive(res))
}
return res
}
}
// ...
// 创建只读的 getter
const readonlyGet = createGetter(false, true)
// 创建只读的 handler,并阻止 set 和 delete 操作
export const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`)
return true // 阻止修改
},
deleteProperty(target, key) {
console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`)
return true // 阻止删除
}
}
createGetter
的标记逻辑是:IS_REACTIVE
和 IS_READONLY
标记在原始对象上并不存在,但当外部代码(如 isReadonly()
)访问它们时,代理对象的 getter
会被触发。getter
会根据创建时传入的 isReadonly
参数,返回对应的布尔值。
我们回到 reactive.ts
,完成 readonly
的实现:
TypeScript
// reactive.ts
import { mutableHandlers, shallowReactiveHandlers, readonlyHandlers } from './baseHandlers'
// 创建一个 readonly 缓存 map
const readonlyMap = new WeakMap()
// ...
function createReactiveObject(target, handlers, proxyMap) {
// reactive 只处理对象
if (!isObject(target)) return target
// 如果遇到重复代理,或是只读对象,无需处理,并返回其自身
if (target[ReactiveFlags.IS_REACTIVE] || target[ReactiveFlags.IS_READONLY]) {
return target
}
// ... (检查 existingProxy 逻辑不变)
// 创建 target 的代理对象
const proxy = new Proxy(target, handlers)
// 存储 target 和代理的关联关系
proxyMap.set(target, proxy)
return proxy
}
// ...
export function readonly(target) {
return createReactiveObject(target, readonlyHandlers, readonlyMap)
}
这样我们就完成了 readonly
的实现。
循环引用
有些人可能会发现我们遇到了循环引用的状态:
rust
ref.ts -> reactive.ts -> baseHandlers.ts -> ref.ts
这个问题在 CommonJS 中需要特别注意和避免,但在现代的 ESM (ES Modules) 中可以正常运作。
什么是循环引用?
在过往的 CommonJS 中,require()
是同步执行的。当模块 A 依赖模块 B,而模块 B 同时又依赖模块 A 时,这会导致其中一个模块在被引入时没有被完全初始化,从而引发运行时错误。
实时绑定 (Live Binding)
ESM 的 import
/export
机制与 CommonJS 完全不同。它导出的不是一个值的拷贝 ,而是一个实时绑定 ,可以把它想象成一个指向原始变量内存地址的指针。
ESM 通过一个巧妙的两阶段过程来处理模块,从而解决了循环引用的问题:
-
第一阶段:解析与绑定
- JavaScript 引擎首先会扫描所有相关的模块文件,解析
import
和export
语句,创建一个完整的"依赖图"。 - 在这个阶段,引擎会为所有
export
的变量、函数、类在内存中创建绑定并分配空间 ,但不会执行任何代码。
- JavaScript 引擎首先会扫描所有相关的模块文件,解析
-
第二阶段:执行与赋值
- 在所有绑定都建立好之后,引擎才开始执行每个模块的主体代码,将实际的函数或值放到之前预留的内存位置中。
- 以我们这次的情况来说:当
baseHandlers.ts
需要import { readonly } from './reactive'
时,它得到的是readonly
这个函数的"实时绑定"(一个内存地址引用)。 baseHandlers.ts
模块(例如createGetter
函数的定义)可以顺利执行完毕。- 之后,
reactive.ts
模块也会执行,将readonly
函数的定义(即函数体)填充到它的绑定中。
关键是执行时机
最关键的一点是:
baseHandlers.ts
里的 createGetter
在定义时,只是引用了 readonly
的绑定,它并没有被立即调用。
get
处理器要等到未来某个代理对象的属性被访问时,才会被真正执行。而到那个时候,所有模块早就完成了第二阶段的执行和赋值。因此,当 get
处理器内部调用 readonly(res)
时,它能访问到完整的、已定义的 readonly
函数,不会有任何问题。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。