likes
comments
collection
share

从源码的角度看 toRef computed 在业务中如何使用

作者站长头像
站长
· 阅读数 20

前言

业务开发中,经常会遇到这种情况。为了减少接口调用,后端会针对功能尽可能的压缩接口数量,可能一个页面上的很多数据都只在一个接口中,但前端页面一般是由多个板块组成,比如表单板块和表格板块,两个板块都会设计成独立的组件,为了便利的使用数据,我们会把数据从后端返回的大对象解构出来,这个时候就遇到了 toRefcomputed 两个 API 的使用场景。

使用

Vue 对计算属性的定义是用来描述依赖响应式状态的复杂逻辑,会基于其响应式依赖被缓存,我们大多数使用 computed 的场景是用来缓存一个复杂计算,如果把 computedtoRef 比较,仅仅用来缓存对象中的某个变量,似乎有点浪费,别急,我们先来看看下面这个 示例(点我跳转)

<script setup>
import { reactive, toRef, computed } from 'vue'

const vnode = reactive({
    name: 'div',
    attrs: {
        class: 'tag',
        style: 'width: 100px'
    }
})

const nameForToRef = toRef(vnode, 'name')
const styleForToRef = toRef(vnode.attrs, 'style')

const nameForComputed = computed(()=> vnode.name)
const styleForComputed = computed(()=> vnode.attrs.style)

function fn(){
  vnode.name = 'span'
  vnode.attrs = {
    class: 'tag',
    style: `width: ${parseInt(Math.random() * 100)}px`
  }
}
</script>

<template>
  <h1>toRef: {{ nameForToRef }} 的 style:{{ styleForToRef }}</h1>
  <h1>computed: {{ nameForComputed }} 的 style:{{ styleForComputed }}</h1>
  <button @click="fn">change</button>
</template>

从源码的角度看 toRef computed 在业务中如何使用

上面这个例子,我们需要解构出 namestyle 两个变量在页面上展示,点击按钮会改变 vnode,直接修改内部的 nameattrs 属性。

不管是基于 toRef 还是 computed 解构出来变量,在模板上展示的都相同,但我们尝试修改一下 vnode 的属性,只基于有 computedstyle 变量触发响应式,基于 toRefstyle 变量依旧是 100px

从源码的角度看 toRef computed 在业务中如何使用

我们从源码的角度分析一下出现这种情况的原因。

源码分析

toRef 的源码稍简单一些

class ObjectRefImpl{
  public readonly __v_isRef = true
  private readonly _object,
  private readonly _key,

  constructor(
    object,
    key,
  ) {
    // 在内部保存一份 vnode.attrs 的引用及要解构的 key 值
    // 此时的 this._object 即 vnode.attrs
    this._object = object
    this._key = key
  }

  get value() {
    // 在外层 style.value 的时候会触发此函数,返回原对象上的值
    // 经过访问原对象 vnode.attrs.style 会把当前环境存入 vnode.attrs.style 的 dep 中
    // 一旦原对象的值变更,会遍历 dep,触发依赖此值的 watch computed 等更新
    const val = this._object[this._key]
    return val
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

function toRef(
  object,
  key
) {
  return new ObjectRefImpl(object, key)
}

const style = toRef(vnode.attrs, 'style')
console.log(style.value) // 触发 ObjectRefImpl 中的 get value()

传入要解构的对象及 key 值,通过 ObjectRefImpl 保存一份原对象的引用。这也就解释了为什么 vnode.attrs = { class: 'tag', style: `width: ${parseInt(Math.random() * 100)}px` } 后,toRefstyle 值没有更新,因为 vnode.attrs 被赋予了一个新值,ObjectRefImpl 内部保存的引用不再和 vnode.attrs 共通,vnode.attrs 发生变更,自然也就和 style.value 无关。我们再看看 computed 为什么可以保证响应式。

以下是 computed源码

export class ComputedRefImpl<T> {
  constructor(
    getter: ComputedGetter<T>
  ) {
    // 通过 ReactiveEffect 注册响应式
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }

  get value() {
    const self = toRaw(this)
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      // .run 执行 getter,同时将当前 computed 环境存放到依赖的响应式变量 dep 中
      // 只要依赖的响应式变量发生变化,就会触发依赖 computed 的环境
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

computedtoRef 不同,computed 是监听内部的响应式变量,对于 vnode.attrs.style 这样深层访问,会逐层监听 attrs -> style 的变更。

总结

如果你的响应式变量不是深层对象,属性仅仅类似于 { a, b, c } 这样简单的基本类型,那可以大胆的使用 toRef,或者可以保证这个深层对象都是 obj.a.b = 1 这样的细粒度变更,使用 toRef 还不会再次创建一个依赖列表,但是在实际业务中,如果对象嵌套层级很深,并且无法保证细粒度更新,建议使用 computed 去解构属性,这样不会丢失响应式