从零到一打造 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」,一起跟日安当同学。

相关推荐
灵感__idea7 小时前
Hello 算法:贪心的世界
前端·javascript·算法
GreenTea8 小时前
一文搞懂Harness Engineering与Meta-Harness
前端·人工智能·后端
killerbasd10 小时前
牧苏苏传 我不装了 4/7
前端·javascript·vue.js
吴声子夜歌10 小时前
ES6——二进制数组详解
前端·ecmascript·es6
码事漫谈10 小时前
手把手带你部署本地模型,让你Token自由(小白专属)
前端·后端
ZC跨境爬虫11 小时前
【爬虫实战对比】Requests vs Scrapy 笔趣阁小说爬虫,从单线程到高效并发的全方位升级
前端·爬虫·scrapy·html
爱上好庆祝11 小时前
svg图片
前端·css·学习·html·css3
橘子编程11 小时前
JavaScript与TypeScript终极指南
javascript·ubuntu·typescript
王夏奇11 小时前
python中的__all__ 具体用法
java·前端·python
叫我一声阿雷吧11 小时前
JS 入门通关手册(45):浏览器渲染原理与重绘重排(性能优化核心,面试必考
javascript·前端面试·前端性能优化·浏览器渲染·浏览器渲染原理,重排重绘·reflow·repaint