从零到一打造 Vue3 响应式系统 Day 27 - toRef、toRefs、ProxyRef、unref

在响应式系统中,reactive 能够将一个对象转换为深层的响应式对象,但是在开发过程中,我们时常会需要用到解构赋值,这时候会导致响应性丢失。

问题解析

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, ref } from '../dist/reactivity.esm.js'

    const state = reactive({
      name: 'a',
      age: 18
    })

    const { name } = state // 解构赋值

    effect(() => {
      console.log(name) // 打印的是 'a',一个普通的字符串
    })

    setTimeout(() => {
      state.name = 'b' // 这里的修改无法被 effect 侦测到
    }, 1000)
  </script>
</body>

执行这段代码,你会发现解构出来的属性会丢失响应式,所以 setTimeout 不会触发更新。

为了解决上述问题,我们通常会用 toRef,让解构出来的变量可以触发响应式更新:

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, ref } from '../dist/reactivity.esm.js'

    const state = reactive({
      name: 'a',
      age: 18
    })

    const name = toRef(state, 'name') // 使用 toRef

    effect(() => {
      console.log(name.value) // 需要通过 .value 访问
    })

    setTimeout(() => {
      state.name = 'b'
    }, 1000)
  </script>
</body>

核心原理

如果这时候去看这个 name 输出的类型:

你会发现它跟我们在使用的 RefImpl 类型不同,它是一个特制的 ObjectRefImpl 类,并且多了两个属性 _object_key,它们分别存储了原始对象、属性名称。

这个 toRef 我们可以知道它接受一个对象以及 key,所以我们可以这样写:

TypeScript 复制代码
// ref.ts
export function toRef(target, key) {
  return {
    get value() {
      return target[key]
    },
    set value(newValue) {
      target[key] = newValue
    }
  }
}

这样其实就可以更新,但官方示例是属于一个类,所以我们也改写成类:

TypeScript 复制代码
class ObjectRefImpl {
  [ReactiveFlags.IS_REF] = true // 标记为 ref
  constructor(public _object, public key) {}

  get value() {
    // 访问 .value 时,代理到原始对象的对应 key
    return this._object[this.key]
  }

  set value(newValue) {
    // 设置 .value 时,代理到原始对象的对应 key
    this._object[this.key] = newValue
  }
}

export function toRef(target, key) {
  return new ObjectRefImpl(target, key)
}

这样就可以将我们解构出来的变量,重新赋予响应性。

toRefs

当需要处理多个属性时,可以使用 toRefs,它会遍历一个 reactive 对象,并将其所有属性都转换为 ref,使用如下:

HTML 复制代码
<body>
  <div id="app"></div>
  <script type="module">
    import { reactive, toRefs, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    // import { reactive, effect, toRef } from '../dist/reactivity.esm.js'

    const state = reactive({
      name: 'a',
      age: 18
    })
    const { name, age } = toRefs(state) // 使用 toRefs

    effect(() => {
      console.log(age.value)
    })

    setTimeout(() => {
      state.age++
    }, 1000)
  </script>
</body>

输出 age 之后,可以看到它也是 ObjectRefImpl 类。

那我们可以知道 toRefs 的实现非常直观,它遍历目标对象的所有 key,并为每一个 key 调用 toRef

TypeScript 复制代码
export function toRefs(target) {
  const res = {}
  for (const key in target) {
    res[key] = new ObjectRefImpl(target, key)
  }
  return res
}

PS:toRefs 源码中还有其他判断逻辑,例如确认传入的是不是响应式对象,我们这边就先省略判断,让它可以触发更新:

虽然 toRefs 解决了响应性丢失的问题,但到处都是 .value,所以我们这边需要两个辅助工具。

unref

unref 是一个简单的辅助函数,如果参数是 ref,它返回 .value;如果不是,则直接返回参数本身。

TypeScript 复制代码
export function unref(value) {
  return isRef(value) ? value.value : value
}

ProxyRef

proxyRefs 可以将一个包含 ref 的对象(例如 toRefs 的返回值)转换为一个特殊的代理。当访问这个代理的属性时,它会自动解包 .value。它跟 reactive 很像,不直接用 reactive 是因为 reactive 是深层响应式的,而 proxyRefs 通常是浅层的。

TypeScript 复制代码
export function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      return unref(res) // 访问时自动 unref
    },
    set(target, key, newValue, receiver) {
      // 这里的 set 也需要处理,如果目标是 ref 而新值不是,应该设置 .value
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

这样就完成了 proxyRefs

今天我们的重点在于:

  • 直接从 reactive 对象中解构,会失去响应性。
  • 使用 toRef 可以为单个属性创建响应式链接。
  • 使用 toRefs 可以将整个对象的所有属性批量转换为 ref,再进行解构。这样每个被解构出来的变量都与原始对象保持了响应式链接。
  • 选择性地使用 unrefproxyRefs 来简化对 .value 的访问。

想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。

相关推荐
xiaofeichaichai3 小时前
Webpack
前端·webpack·node.js
问心无愧05133 小时前
ctf show web入门111
android·前端·笔记
唐某人丶3 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程
智码看视界3 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
JS菌3 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
excel5 小时前
HLS TS 文件损坏的元凶:Git 提交与拉取
前端
Aphasia3115 小时前
https连接传输流程
前端·面试
徐小夕5 小时前
万字长文!千万级文档 RAG 知识库系统落地实践
前端·算法·github
梦梦代码精5 小时前
2026年PHP开源商城系统实测对比:架构、多商户、商用授权,谁才是真·省心?
vue.js·docker·架构·开源·代码规范