揭秘ref的背后——真滴没有那么难

前言

使用过vue3的同学应该对于ref并不陌生, 但是不知道大家有没有想过vue3的Composition API中已经有了reactive为什么还要再加一个ref呢?那么本着知其然知其所以然的原则本文将从以下几个方面展开对ref的研究。

Tips

不知道大家在使用ref的时候有没有这样的一个困惑,就是输出ref值的时候信息量太大,像这样每次还需要将其展开才能看到我们设置的值,很是不便。

那么有没有一种方法能将其简化呢?其实vue有考虑到这一点,我们只需要将Chrome的DevTools中的Enable custom formatters这一选项勾选上就会发现神奇的一幕。

为什么要入ref

vue3中使用Proxy来进行数据劫持,但是Proxy并不支持对原始值(基本数据类型)进行劫持,那如果想要声明一个原始值的响应式数据该怎么办呢?这就是为什么要引入ref的原因,因为要实现对原始值的数据响应式。

ref实现

了解了ref出现的原因后,下面来实现一个简单的ref。因为Proxy只支持对非原始值(引用类型)的数据劫持,那这时候我们可以用非原始值类型去包裹一下原始值,然后再使用reactive包裹完成数据的响应式,ref其实就是借助reactive实现的。

通过观察ref的基本结构我们可以得出:

  1. 通过ref包裹的数据是响应式的
  2. 会返回一个包含value属性的Ref对象

于是就可以写出以下代码:

js 复制代码
function myRef(val) {
  let obj = {
    value: val,
  }
  return reactive(obj);
}

const count = myRef(0)
setTimeout(() => {
  count.value = 1 // 视图也会同步更新
}, 1000)

这样也能按照预期工作,但是现在由于是使用reactive来实现的ref,如何对它们进行区分呢?也就是如何区分refreactive声明的数据,其实也很简单,无非是做判断,在vue源码中使用了__v_isRef来做判断,这里我们就不写那么复杂,我们可以在myRef函数中使用Object.defineProperty方法给obj定义一个不可枚举且不可写的属性__v_isRef并且值为true,这样就可以通过检查对象中是否有__v_isRef属性来判断是否是一个ref数据了。

js 复制代码
function myRef(val) {
  let wrap = {
    value: val
  }
  Object.defineProperty(obj, "__v_isRef", {
    value: true
  })
  return reactive(wrap)
}

ref解决数据响应丢失的问题

ref还可以解决数据响应丢失的问题,当我们在setup中将数据暴露到模板中时如果使用了扩展运算符,像这样:

js 复制代码
  setup() {
    const obj = reactive({
      name: "xxx",
      age: 20
    })

    return {
      ...obj
    }
  }

然后直接在模版中使用obj中的属性会丢失其响应式,也就是说修改obj这个响应式数据时视图并不会重新渲染。

js 复制代码
  setup() {
    const obj = reactive({
      name: "xxx",
      age: 20
    })

    setTimeout(() => {
      obj.name = "sss"
    }, 1000)
    return {
      ...obj
    }
  }

这是由于使用了扩展运算符导致的,在 setup() 函数中返回的对象会暴露给模板和组件实例,而使用了扩展运算符就相当于是返回了一个普通对象,它并不具备任何响应式能力。

js 复制代码
return {
  ...obj
}
// 等价于
return {
    name:"xxx",
    age:20
}

所以要解决这个问题这里的关键点就在于如何将返回的普通对象与响应式数据关联起来,无疑应该使用代理。

js 复制代码
const obj=reactive({
  name:"xxx",
  age:20
})

let newObj={
  name: {
    get value() {
      return obj.name
    }
  },
  age: {
    get value() {
      return obj.age
    }
  }
}

我们可以把newObj看作是要返回的对象,让newObj拥有与obj相同的属性并且每个属性值都是一个对象,然后定义一个名为value的getter方法返回obj中对应的属性值,那么当读取value时其实读取的是obj对象下对应的属性值,这样一来就实现了普通对象与响应式数据之间的关联。

简化一下上面的代码就变成了toRef函数:

js 复制代码
function toRef(obj,key){
  let newObj={
    get value(){
      return obj[key]
    }
  }
  return newObj
}
let newObj = myToRef(obj,"name")
console.log(newObj.value) // "xxx"

但是这样写未免太麻烦了,如果需要对一个对象中的多个属性进行转换的话就要写大量的toRef,于是就诞生了toRefs,它可以帮我们完成对一个对象的转换。实现它也很简单,只需要循环取出对象中的key然后依次进行toRef即可。

js 复制代码
function toRefs(obj) {
  let newObj = {};
  for (let key in obj) {
    newObj[key] = toRef(obj, key)
  }
  return newObj;
}
let newObj = toRefs(obj)
console.log(newObj.name.value) // "xxx"

到此为止响应式丢失的问题就已经解决了,因为toReftoRefs返回的都是一个ref数据,所以还需要加上一个前面提到过的__v_isRef标识。

js 复制代码
function toRef(obj,key){
  let newObj={
    get value(){
      return obj[key]
    }
  }
  Object.defineProperty(newObj, '__v_isRef', {
     value: true
  })
  return newObj
}

自动脱ref

当我们在模板中使用ref时并不需要添加value属性,但是根据上面的例子来看访问属性都必须带上value属性,这会增加用户的心智负担因为我们通常习惯直接在模板中直接使用数据。那么想要实现这个功能还是要用到代理。

js 复制代码
    function proxyRefs(target) {
      return new Proxy(target, {
        get(target, key) {
          let val = target[key]
          return val.__v_isRef ? val.value : val
        }
      })
    }
    const newOobj = toRefs(obj)
    const data = proxyRefs(newObj)
    console.log(data.name) // xxx

只需要判断属性值是否有__v_isRef这个标识,有的话加上.value没有的话直接返回,是不是感觉挺简单!

相关推荐
Keya11 分钟前
MacOS端口被占用的解决方法
前端·后端·设计模式
RainbowSea16 分钟前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 05
vue.js·spring boot·后端
moyu8419 分钟前
解密Vue组件中的`proxy`:此Proxy非彼Proxy
前端
用户849137175471622 分钟前
为什么大模型都离不开SSE?带你搞懂第1章〈SSE技术基础与原理〉
前端·网络协议·llm
顾林海25 分钟前
从"面条代码"到"精装别墅":Android MVPS架构的逆袭之路
android·面试·架构
随笔记25 分钟前
react中函数式组件和类组件有什么区别?新建的react项目用函数式组件还是类组件?
前端·react.js·typescript
在星空下28 分钟前
Fastapi-Vue3-Admin
前端·python·fastapi
FogLetter29 分钟前
面试官问我Function Call,我这样回答拿到了Offer!
前端·面试·openai
Juchecar30 分钟前
CSS布局模式详解 - 初学者完全指南
前端
emojiwoo32 分钟前
React 状态管理:useState 与 useDatePersistentState 深度对比
前端·javascript·react.js