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

相关推荐
WindrunnerMax3 小时前
从零实现富文本编辑器#8-浏览器输入模式的非受控DOM行为
前端·前端框架·github
sjin3 小时前
React源码 - 关键数据结构
前端·react.js
旺仔牛仔QQ糖3 小时前
IntersectionObserver 异步交叉观察器
前端
猪猪拆迁队3 小时前
基于ECS架构的Canvas画布编辑器
前端
Q_Q19632884754 小时前
python+vue的在线租房 房屋租赁系统
开发语言·vue.js·spring boot·python·django·flask·node.js
不如喫茶去4 小时前
VUE查询-历史记录功能
前端·javascript·vue.js
持梦远方4 小时前
重生之我拿捏Linux——《三、shell脚本使用》
前端·chrome
行走在顶尖4 小时前
代码截断运行逻辑
前端
武天4 小时前
说说你对slot的理解?slot使用场景有哪些?
vue.js