【拆解Vue3】ref是如何实现的?

516 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

本篇内容基于【拆解Vue3】reactive是如何实现的?实现。

有了reactive为什么还需要ref?

学习JavaScript,最先接触到的知识就是数据类型。从广义上讲,我们可以把数据类型分为两种:

  1. 原始类型(7种):number, bigint, string, boolean, null, undefined, symbol
  2. 引用类型:object

typeof null 的结果是 object,这是一个官方承认的错误,为了兼容性才保留了下来。

处于程序执行效率的考虑,原始类型被保存在栈空间中,而引用类型被保存在堆空间中。这在赋值时产生了显著的差异,原始类型采用了值传递的方式,而引用类型传递的是应用。举个例子来说明这点。

function add(a, b) {
    a += 1
    b += 1
}
let a = 1, b = 1
add(a, b)
console.log(a, b)

执行这段代码,不难发现尽管执行了add(a, b),但最后输出时,ab的值没有发生改变,仍然都是1。值传递的方式意味着函数内的a b与函数外的a b实际上是相互独立的。接下来,基于这个例子,我们改为引用传递看看效果。

function add(a, b) {
    a.val += 1
    b.val += 1
}
let a = { val: 1 }, b = { val: 1 }
add(a, b)
console.log(a.val, b.val)

这里的a.val b.val的值都变为了2,这说明引用传递时,函数内的a b与函数外的a b实际上是相同的。

上面我们简单复习了一下不同类型的非响应式的数据,在进行赋值时产生的差异。接着让我们想想,这种差异是否会对响应式产生影响?

任何一种响应式数据,必须要满足在修改时能够触发对应的副作用函数,这点对于原始类型的响应式来说也一样。为了实现这一点,我们必须让原始数据类型通过引用传递的方式进行赋值,引入reactive对原始类型数据进行包装。在Vue3的官方文档中,我们也找到了对应的表述:

ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref 名称的来源。

ref

还是先来看看ref是如何使用的。

const count = ref(0)

顺着上一小节的思路,我们引入reactive对原始类型数据进行包装,但由于reactive基于Proxy实现,只能给对象提供响应式能力,这里我们替开发者来做一层封装。

function ref(val) {
  const wrapper = {
    value: val
  }
  return reactive(wrapper)
}

接着,我们继续基于上一小节的例子来试试,使用ref会有什么响应式效果。

const a = ref(0)
const b = ref(0)
effect(() => {
  const c = a
  const d = b
  add(c, d)
  function add (a, b) {
    a.value += 1
    b.value += 1
  }
  console.log('a = ', a.value, ', b = ', b.value)
})

QQ截图20220808134432.png

从上图可以看到,第一次执行effect的结果,a b的值加上了1.第二次通过修改a.value再次触发effect,也得到了预期的结果。到这里我们已经实现了ref的基本功能。最后需要考虑一个细节问题,ref实际上返回的是一个reactive,但在实际使用时,我们需要有区分二者的能力。为了实现这一点,我们使用defineProperty属性标志和属性描述符)给ref打一个标记__v_isRef

function ref(val) {
  const wrapper = {
    value: val
  }
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true
  })
  return reactive(wrapper)
}

toRef与toRefs

正如Vue3文档中所说,当我们想使用大型响应式对象的一些属性时,可能想使用ES6的解构语法来获取我们想要的属性。但这会导致属性的响应性丢失,如下面这个例子所示。

const state = reactive({
  foo: 1,
  bar: 2
})
let { foo } = state

QQ截图20220808150605.png

这是因为解构相当于返回了一个普通属性,普通属性当然不包含响应式的能力。为了方便理解,我们给出一个例子来说明解构的实质。

let { foo } = state // (1)
let foo = state.foo // (2)

(1)处的解构语法,实质上执行的是(2)处的赋值,二者是完全等价的。到这就很清楚了,又是由值传递引发了预期之外的结果。我们沿用上一节的实现ref的思路,通过包装,让其基于引用传递来完成响应式。

function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key] // (3)
    },
    set value(val) {
      obj[key] = val // (4)
    },
  }
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true
  })
  return wrapper
}

这里和ref不同的是,toRef接收的就是响应式对象,我们只需要拦截这个响应式对象的获取和读取操作,将操作映射到响应式对象的属性上去。实现效果如下图所示。

QQ截图20220808153855.png

实现了toRef,接下来的toRefs就很好完成了。我们只需要遍历响应式对象中的每一个属性,给它们套上toRef就可以执行了。

function toRefs(obj) {
  const ret = {}
  for(const key in obj) {
    ret[key] = toRef(obj, key)
  }
  return ret
}

const state = reactive({
  foo: 1,
  bar: 2
})
const obj = { ...toRefs(state) }

QQ截图20220808163138.png

这里又有一个蛋疼的问题,在语义上toRef为了和ref保持一致,我们需要通过foo.value才能对响应式数据的值进行读取或修改,而我们在Vue模板上使用ref时是不需要value就可以直接操作原始类型的响应式对象的。为了避免给用户造成不必要的心智负担,我们要想办法去掉value

我们的想法恰好对应了Vue3官方文档中关于Ref解包的概念。原话是,当ref作为渲染上下文(从setup()中返回的对象)上的属性返回并可以在模板中被访问时,它将自动浅层次解包内部值。这里再明确一下要实现的是针对从setup()中返回的对象,去掉其value。用proxy拦截对象的get set我们已经很熟悉了。

function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      return value.__v_isRef ? value.value : value // (5)
    },
    set(target, key, newValue, receiver) {
      const value = target[key]
      if(value.__v_isRef) { // (6)
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

我们之前设置的__v_isRef标签起作用了,通过__v_isRef标签,我们就能判断哪个属性是ref。在(5)处,我们拦截get操作,当读取ref属性时,直接返回.value的结果。同理,在(6)处,我们拦截set操作,当修改ref属性时,直接使用.value去接赋值。接下来,我们验证一下proxyRefs能否按预期执行。

const count = ref(0)
let setup = { count } 
/** 上例相当于
 *  setup() {
 *    const count = ref(0)
 *    return {
 *      count
 *    }
 *  }
 */
setup = proxyRefs(setup)

QQ截图20220808173625.png

在Vue组件中,setup()返回对象时,会自动使用proxyRefs()进行处理,以此来达到Ref解包的目的。

参考资料