likes
comments
collection
share

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

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

前言

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

Tips

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

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

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

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

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

为什么要入ref

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

ref实现

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

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

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

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

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数据了。

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

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

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

  setup() {
    const obj = reactive({
      name: "xxx",
      age: 20
    })

    return {
      ...obj
    }
  }

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

  setup() {
    const obj = reactive({
      name: "xxx",
      age: 20
    })

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

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

return {
  ...obj
}
// 等价于
return {
    name:"xxx",
    age:20
}

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

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函数:

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即可。

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标识。

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

自动脱ref

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

    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没有的话直接返回,是不是感觉挺简单!