🔥由浅入深,揭秘vue3中ref、setup、template三者的藕断丝连!

173 阅读4分钟

前言

大家好,今天打算写一个vue3中的ref函数,当然不单单是封装一个ref函数这么简单,还会在此基础上引申出vuetemplate模板中读取及设置ref值时怎么做到省略.value的写法,要深入理解其中原理又得引申出setup函数给template暴露属性时都做了什么,从而理解三者(setup、template、ref) 之间的藕断丝连。我们这里不讲源码,旨在用最通俗的方式让大家理解其中的实现思路。

ref函数揭秘

vue官方文档ref是这样介绍的:如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。

对其有了基本了解后,我们创建一个myvue.js文件,尝试自己封装一个ref函数:

import { reactive } from 'vue'
export const ref = (value) => {
  const wrapper = {
    value
  }
  return reactive(wrapper)
}

封装好后,我们尝试在vue页面中引入使用:

<template>
  <div>
    <span>当前值:{{ count.value }}</span>
    <button @click="increment">加一</button>
  </div>
</template>

<script setup>
// 自己封装的ref
import { ref } from './hooks/myvue'

const count = ref(0)
const increment = () => {
  count.value++
}
</script>

当我们点击加一按钮时,页面显示的count.value值也同样能够正常刷新。But,还没完!细心的同学可能已经发现,在template模板中使用count变量时,需要.value才行,我们想要的效果是像vue提供的ref函数一样,不用.value也能正常读取值。

template中省略.value揭秘

其实不难,我们只需在前文自己封装的ref函数里增加一行代码即可!

import { reactive } from 'vue'
export const ref = (value) => {
  const wrapper = {
    value
  }
  /**
   * 在wrapper上定义一个不可修改且不可迭代的私有属性,
   * vue内部会通过这个__v_isRef私有属性判断是否是ref响应式数据
   */
  // 新增代码
  Object.defineProperty(wrapper, '__v_isRef', {value: true}) 
  return reactive(wrapper)
}

这样我们就能在template中省略.value写法了!!!

到这,虽然功能实现了,但我们不能知其然而不知其所以然,下面说说其实现原理。

setup返回值暴露原理揭秘

我们这次不使用<script setup>语法糖,采用原始的setup函数暴露属性template模板使用:

import { ref } from './hooks/myvue'
export default {
  setup() {
    const count = ref(0)
    const increment = () => {
      count.value++
    }
    // 将 ref 暴露给模板
    return {
      count,
      increment
    }
  }
}
</script>

从以上可看出,setup的返回值是一个对象,而这个对象中的属性之所以能够在template中直接使用,是归功于webpack/vite之类的编译打包工具,在.vue文件编译成js文件中的render函数执行时,依然是通过对象.属性(比如:obj.count)的形式读取setup返回的对象属性,只不过这个setup函数返回的对象经过了Proxy包装,变成了一个响应式对象,在读取/设置属性时,在get/set方法中拦截判断是否为ref响应式对象,如果是则会自动补全.value并返回/设置相应值。

下面我们通过编写一个proxyRefs包装函数,更直观的了解其中原理。

const proxyRefs = (obj) => {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 读取target[key]的值
      const value = Reflect.get(target, key, receiver)
      // __v_isRef为我们标记为ref的属性
      return value.__v_isRef ? value.value : value
    },
    set(target, key, newValue, receiver) {
      const value = target[key]
      if(value.__v_isRef) {
        // 是ref则设置.value值
        value.value = newValue
        // 返回设置成功
        return true
      }
      // 设置target[key]的值,并返回是否设置成功
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

以上就是我们实现的setup返回对象包装方法,之后我们使用ref时就不再需要.value了!主要是通过Proxyget方法拦截读取操作,如果读取的值是ref则返回其.value值,如果不是则直接返回原始值;set拦截也是同理,目的是在设置ref值时也能省略.value写法。

下面我们通过一个例子测试一下:

<script>
import { ref, proxyRefs } from './hooks/myvue'
export default {
  setup() {
    const count = ref(0)
    const increment = () => {
      count.value++
    }
    const state = {
      count,
      increment
    }
    
    const state2 = proxyRefs(state)
    console.log(state2.count) // 0
    state2.count++
    console.log(state.count.value)  // 1
    // 将 ref 暴露给模板
    return state
  }
}
</script>

以上我们把setup返回对象通过刚刚我们自己写的proxyRefs方法做一层包装,并赋值给state2,并且我们通过state2.count成功读取和设置了state中的count值。

总结

首先,封装ref函数,并创建一个具有value属性wrapper对象,并把传入的参数赋值给value属性,最后再把wrapper对象通过reactive响应式API,包装成一个深层的响应式对象

其次,为了使ref能够省略.value的写法,我们给wrapper包装对象设置了一个私有属性__v_isRef,由于vue内部对setup函数返回的对象做了Proxy拦截包装,所以在template模板中读取ref的值时,可以省略.value读取及设置ref的值。

最后,我们手写了一个在读取或设置ref的值时,可以省略.value的包装函数proxyRefs,深入了解setup函数给模板暴露属性时,是如何包装返回的对象给模板使用。