无限极服务
146.43MB · 2025-10-24
在开始 readonly 之前,我们先补充一下 Proxy 的知识:
Proxy 是实现 reactive、readonly 等功能的核心。它会在目标对象前架设一个“代理”或“拦截层”,让我们有机会对外界的访问操作进行自定义处理。
Proxy 的工作模式可以想象成一个保安:
target) :是公司内部的办公室。proxy) :保安本人。handler) :是保安的应对手册,里面写了访问对象时该如何处理的逻辑。任何外部代码(访客)要访问对象属性(进办公室)都需要经过 Proxy(保安),Proxy 会查询 handler(保安手册)来决定如何响应。
在 handler 中,最关键的陷阱 (trap) 之一就是 get。get(target, key, receiver) :这个陷阱的触发时机是当代码试图读取代理对象属性时。即使原始对象上并不存在这个属性,它也可以通过 handler 的规则去处理。
了解这些之后,可以开始实现了!
readonly 只接受对象参数。在前面的文章中我们提到,ref 如果传入的是对象,那它内部也会调用 reactive。因此,在 readonly 的实现中,我们只要能正确处理 reactive 对象(或普通对象)就可以。
<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 仍然会接收到响应式的触发更新。
setTimeout(() => {
readonlyState.a++ // 尝试修改 readonly 对象
}, 1000)
但如果你修改的是
readonly 对象本身,那就会在控制台收到警告。
查看这个
readonly 对象,可以发现它很像一个 reactive 对象,是通过 _isReadonly 标记来判断的。这跟我们上一个章节在实现 shallow 时的思路特别像。
首先,我们先在 ref.ts 中增加枚举标记,分别是 IS_REACTIVE 以及 IS_READONLY:
// ref.ts
export enum ReactiveFlags {
IS_REF = '__v_isRef',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly'
}
接着调整一下 reactive.ts,我们移除原有的 Set 检查,改为通过标记来判断是否需要重复代理。
// 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。
// 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 的实现:
// 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 的实现。
有些人可能会发现我们遇到了循环引用的状态:
ref.ts -> reactive.ts -> baseHandlers.ts -> ref.ts
这个问题在 CommonJS 中需要特别注意和避免,但在现代的 ESM (ES Modules) 中可以正常运作。
在过往的 CommonJS 中,require() 是同步执行的。当模块 A 依赖模块 B,而模块 B 同时又依赖模块 A 时,这会导致其中一个模块在被引入时没有被完全初始化,从而引发运行时错误。
ESM 的 import/export 机制与 CommonJS 完全不同。它导出的不是一个值的拷贝,而是一个实时绑定,可以把它想象成一个指向原始变量内存地址的指针。
ESM 通过一个巧妙的两阶段过程来处理模块,从而解决了循环引用的问题:
第一阶段:解析与绑定
import 和 export 语句,创建一个完整的“依赖图”。export 的变量、函数、类在内存中创建绑定并分配空间,但不会执行任何代码。第二阶段:执行与赋值
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」,一起跟日安当同学。
2025-10-25
2025/26 冬春航季即将启动,国航 C919 新增广州、西安、长沙等新航点
部分 Win11 23H2,Windows Server 2016/2022 安装微软 10 月累积更新失败,严重至“变砖”