ZuB1M1H.png

在开始 readonly 之前,我们先补充一下 Proxy 的知识:

Proxy

Proxy 是实现 reactivereadonly 等功能的核心。它会在目标对象前架设一个“代理”或“拦截层”,让我们有机会对外界的访问操作进行自定义处理。

拦截与代理

Proxy 的工作模式可以想象成一个保安:

  • 目标对象 (target) :是公司内部的办公室。
  • 代理对象 (proxy) :保安本人。
  • 处理器 (handler) :是保安的应对手册,里面写了访问对象时该如何处理的逻辑。

任何外部代码(访客)要访问对象属性(进办公室)都需要经过 Proxy(保安),Proxy 会查询 handler(保安手册)来决定如何响应。

handler 中,最关键的陷阱 (trap) 之一就是 getget(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>

day29-01.png

如果你设置一个 readonly 对象,当修改原始的 reactive 对象时,readonly 仍然会接收到响应式的触发更新。

setTimeout(() => {
  readonlyState.a++ // 尝试修改 readonly 对象
}, 1000)

day29-02.png 但如果你修改的是 readonly 对象本身,那就会在控制台收到警告。

day29-03.png 查看这个 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_REACTIVEIS_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 时,这会导致其中一个模块在被引入时没有被完全初始化,从而引发运行时错误。

实时绑定 (Live Binding)

ESM 的 import/export 机制与 CommonJS 完全不同。它导出的不是一个值的拷贝,而是一个实时绑定,可以把它想象成一个指向原始变量内存地址的指针

ESM 通过一个巧妙的两阶段过程来处理模块,从而解决了循环引用的问题:

  • 第一阶段:解析与绑定

    • JavaScript 引擎首先会扫描所有相关的模块文件,解析 importexport 语句,创建一个完整的“依赖图”。
    • 在这个阶段,引擎会为所有 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」,一起跟日安当同学。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]