vue3 中 ref 或reactive 到底该怎么用

1,521 阅读6分钟

前言

vue3中对响应式数据的声明官方给出了ref()reactive()这两种方式

可这两个方式第一次看见学习又该怎么使用呢,响应式数据为什么会给两个方法呢,又该分别传什么参数,到底有啥不同。带着这些疑问看了文档,总结出以下问题:

ref究竟是什么?

ref(对象,数组,数组对象) 可行吗?

reactive方法又是什么?

ref与reactive的区别有什么?

toRef是什么? toRefs又是什么?

ref

官方定义 :接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

先看看ref方法内部结构

类型声明:

interface Ref<T> {
  value: T
}

function ref<T>(value: T): Ref<T>

相当于ref()方法返回的是Ref类型 Ref类型中包含value属性

其实知道了ref()方法的内部声明,我们就已经可以知道ref()方法可以传入基本类型和对象,数组,数组对象

无非就是把ref()方法的参数值赋值给Ref类型的value属性

示例:

const count = ref(0)
const obj = ref({name:'我是对象'})
const array = ref([])
console.log(count.value) 	//0    这里count 相当于变成了一个Ref对象   
console.log(obj.value.name)     //我是对象
console.log(array.value)	//[]

这里有个小注意点:

在Vue中template里使用ref值不用通过value获取

在JS中使用ref的值必须通过value来获取

所以想用ref来创建数组对象并用TS类型规范就可以如下写出

interface  HumanBeings {
    name:string,
    age:number,
    sex:string,
    address?:string
}
//这里通过TS泛型来定义类型
const students:Ref<HumanBeings[]> = ref<HumanBeings[]>([  
   {
      name:'小明',
      age:18,
      sex:'男'
   },
   {
      name:'大山',
      age:20,
      sex:'男',
      address:'四川省成都市'
   }
])

拓展:这里引出一个问题

如果通过ref()函数中嵌套ref()函数

类似:

const count = ref(ref(ref(ref(2))))

这是一个好几层的嵌套,每一层返回的都是Ref类型,按理来说应该是count.value.value.value.value 才会是 number,但是在 vscode 中,鼠标指向count.value 这个变量后,提示出的类型就是 number 。这里推荐另一篇大佬的文章 Vue3 跟着尤雨溪学 TypeScript 之 Ref 类型从零实现 。非常详细的解释了是如何做到的,需要一定的TypeScript 基础

何为reactive()方法

周所周知Vue2.0是通过Object.defineProperty()来实现对属性的劫持,达到数据驱动的目的(只能够监听定义时的属性,不能监听新加的属性);而Vue3.0是通过Proxy代理对象,在Vue3.0中有两个重要的数据响应式监听方法 ref 和 reactive ,当然这两个方法就是通过Proxy来实现的;

// ref和reactive底层实现

// reactive
function reactive (obj) {
  // 首先判断obj的类型
  if (typeof obj === 'object') {
    if (obj instanceof Array) {
      // 如果是数组那么取出数组中的每一个元素,判断每一个元素是否又是对象,如果是对象也需要包装成Proxy
      obj.forEach((item, index) => {
        if (typeof item === 'object') {
          item[index] = reactive(item)
        }
      })
    } else {
      // 如若是对象 取出对象中的每一个值 判断对象的属性的值是否又是对象,如果是也需要包装成Proxy
      for (let key in obj) {
        if (typeof obj[key] === 'object') {
          obj[key] = reactive(obj[key])
        }
      }
    }
  } else {
    console.warn(`您传入的内容: ${obj}不是一个对象`)
  }
  return new Proxy(obj, {
    get (obj, key) {
      console.log("获取值")
      return obj[key]
    },
    set (obj, key, value) {
      console.log("改变值")
      obj[key] = value
      return true  // 表示当前操作成功 继续执行下一步
    }
  })
}


// ref

function ref(val) {
  return reactive({value: val})
}

从底层代码实现就可以看出

ref方法本质上是其实还是reactive方法,我们给ref传入的值将它会转成ref(xxx) ----> reactive({value:xxx})

reactive方法传入的值必须是对象或者数组(json/arr),所以当我们只想让某一个变量实现响应式的时候会变的很麻烦,我们就可以采用ref方法来实现将简单的基本类型封装为对象再来进行监听

关于官方文档中提到的:如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象,这样的话何不直接用reactive方法来定义对象呢。

所以我们常常可以在其他文章中看到:reactive方法更推荐去定义复杂的数据类型,ref更推荐定义基本类型,当然ref也可以定义数组和对象

拓展 toRef 官方demo如下

可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接

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

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

当你要将 prop 的 ref 传递给复合函数时,toRef 很有用:

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

即使源 property 不存在,toRef 也会返回一个可用的 ref。这使得它在使用可选 prop 时特别有用,可选 prop 并不会被 toRefs 处理。

拓展toRef底层实现原理

//检查值是否为一个 ref 对象		
function isRef(r) { 
    return Boolean(r && r.__v_isRef);
}

//创建ref类
class ObjectRefImpl {		
    constructor(_object, _key) {			
        this._object = _object;    //这里将_object 的引用地址 赋给 this._object 
        this._key = _key;
        this.__v_isRef = true;   //用于判断是否是ref对象
    }
    
    //获取value属性		
    get value() {		
        return this._object[this._key];
    }
    
    //设置value属性
    //改变this._object属性值 原来的 _object的属性值也会发生改变
    set value(newVal) {		
        this._object[this._key] = newVal;     
    }
}
function toRef(object, key) {
    const val = object[key];
    return isRef(val) ? val : new ObjectRefImpl(object, key);
}

拓展toRefs官方Demo如下

将响应式对象转换为 普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref

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

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:
{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

当从组合式函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行解构/展开:

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

  // 操作 state 的逻辑

  // 返回时转换为ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以在不失去响应性的情况下解构
    const { foo, bar } = useFeatureX()
    return {foo,bar}
  }
}

toRefs 只会为源对象中包含的 property 生成 ref。如果要为特定的 property 创建 ref,则应当使用 toRef

看文档的例子会发现官方总结的相当到位:

  • 将响应式对象(reactive封装)转成普通对象
  • 普通对象的每个属性都是对应的ref
  • 两者保持引用关系

toRefs方法实现

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

最后

本人也是刚刚开始学习Vue3,总结一下自己所想搞懂的一些问题,如果有什么不足或者不正确的地方,欢迎留言指正~~