vue3中,ref和reactive 都可以将数据作为响应式数据,在代码中,ref比reactive多带着一个 .value,常常让人觉得代码过长,显得难看,而在模板中则不需要。所以在使用时,一开始都更偏向于reactive?那为啥尤大还是更推荐ref?


原因如下:
  1. 局限性问题: reactive本身存在一些局限性,可能会在开发过程中引发一些问题。这需要额外的注意力和处理,否则可能对开发造成麻烦。

  2. 数据类型限制: reactive声明的数据类型仅限于对象,而ref则更加灵活,可以容纳任何数据类型。这使得ref更适合一般的响应式状态的声明。

  3. 官方推荐: 官方文档强烈建议使用ref()作为声明响应式状态的首选。这是因为ref更简单、更直观,同时避免了reactive可能引发的一些问题。

 

refreactive
支持基本数据类型 + 引用数据类型只支持对象和数组(引用数据类型
在 <script> 和 <template> 使用方式不同(在 <script> 中要使用 .value在 <script> 和 <template> 中无差别使用
重新分配一个新对象不会失去响应重新分配一个新对象会丢失响应性
需要使用 .value 访问属性能直接访问属性
传入函数时,不会失去响应将对象传入函数时,失去响应
解构对象时会丢失响应性,需使用 toRefs解构时会丢失响应性,需使用 toRefs

reactive使用不当会失去响应性

使用 reactive 时,如果不当使用,可能导致响应性失效,带来一些困扰。这可能让开发者在愉快编码的同时,突然发现某些操作失去了响应性,不明所以。因此,建议在不了解 reactive 失去响应的情况下慎用,而更推荐使用 ref,如:

// 第一种失去响应式
let state = reactive({ count: 0 })
// 这个赋值将导致 state 失去响应,修改的内容在template 里不会变化
state = { count: 1 }

// 解决:
// 不替换整个对象
state.count = 2

// Object.assign
state = Object.assign(state, { count: 1 })


// 第二种失去响应式

let state = reactive({ count: 0 })
let n = state.count
// 不会影响state里的count,依然为0
n += 1
// 解决:
// 如果使用ref则不会

// 第三种失去响应式
let state = reactive({ count: 0 })
// 普通解构,count 和 state.count 失去了响应性连接
let { count } = state
count += 1 // state.count 值依旧是 0

reactive 重新赋值丢失响应是因为引用地址变了,被 proxy 代理的对象已经不是原来的那个,所以丢失响应了。其实 ref 也是一样的,当把 .value 那一层替换成另外一个有着 .value 的对象也会丢失响应。ref 定义的属性等价于 reactive({ value: xxx })

另外,说使用 Object.assign 为什么可以更新模板:

Object.assign 解释是这样的:如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。

那个解决方法里不用重新赋值,直接 Object.assign(state, { count: 1 }) 即可,所以只要 proxy 代理的引用地址没变,就会一直存在响应性


ref 的内部工作原理

// 深响应式
exportfunction ref(value?: unknown) {
  return createRef(value, false)
}

// 浅响应式
exportfunction shallowRef(value?: unknown) {
  return createRef(value, true)
}

function createRef(rawValue: unknown, shallow: boolean) {
  // 如果传入的值已经是一个 ref,则直接返回它
  if (isRef(rawValue)) {
    return rawValue
  }
  // 否则,创建一个新的 RefImpl 实例
  returnnew RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  // 存储响应式的值。我们追踪和更新的就是_value。(这个是重点)
  private _value: T
  // 用于存储原始值,即未经任何响应式处理的值。(用于对比的,这块的内容可以不看)
  private _rawValue: T

  // 用于依赖跟踪的 Dep 类实例
  public dep?: Dep = undefined
  // 一个标记,表示这是一个 ref 实例
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    // 如果是浅响应式,直接使用原始值,否则转换为非响应式原始值
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 如果是浅响应式,直接使用原始值,否则转换为响应式值
    this._value = __v_isShallow ? value : toReactive(value)

    // toRaw 用于将响应式引用转换回原始值
    // toReactive 函数用于将传入的值转换为响应式对象。对于基本数据类型,toReactive 直接返回原始值。
    // 对于对象和数组,toReactive 内部会调用 reactive 来创建一个响应式代理。
    // 因此,对于 ref 来说,基本数据类型的值会被 RefImpl 直接包装,而对象和数组
    // 会被 reactive 转换为响应式代理,最后也会被 RefImpl 包装。
    // 这样,无论是哪种类型的数据,ref 都可以提供响应式的 value 属性,
    // 使得数据变化可以被 Vue 正确追踪和更新。
    // export const toReactive = (value) => isObject(value) ? reactive(value) : value
  }

  get value() {
    // 追踪依赖,这样当 ref 的值发生变化时,依赖这个 ref 的组件或副作用函数可以重新运行。
    trackRefValue(this)
    // 返回存储的响应式值
    returnthis._value
  }

  set value(newVal) {
    // 判断是否应该使用新值的直接形式(浅响应式或只读)
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    // 如果需要,将新值转换为非响应式原始值
    newVal = useDirectValue ? newVal : toRaw(newVal)
    // 如果新值与旧值不同,更新 _rawValue 和 _value
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      // 触发依赖更新
      triggerRefValue(this, DirtyLevels.Dirty, newVal)
    }
  }
}

ref核心是返回响应式且可变的引用对象,而reactive核心是返回的是响应式代理,这是两者本质上的核心区别,也就导致了ref优于reactive,我们接着看下reactive源码实现。

reactive 的内部工作原理

reactive 是一个函数,它接受一个对象并返回该对象的响应式代理,也就是 Proxy

function reactive(target) {
  if (target && target.__v_isReactive) {
    return target
  }

  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

function createReactiveObject(
  target,
  isReadonly,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  if (!isObject(target)) {
    return target
  }

  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  const proxy = newProxy(target, baseHandlers)
  proxyMap.set(target, proxy)
  return proxy
}

reactive 通过 new Proxy(target, baseHandlers) 创建了一个代理。这个代理会拦截对目标对象的操作,从而实现响应式。


到这里,我们可以看出 ref 和 reactive 在声明数据的响应式状态上,底层原理是不一样的。ref 采用 RefImpl对象实例,reactive采用Proxy代理对象。

当你使用  new RefImpl(value)  创建一个  RefImpl  实例时,这个实例大致上会包含以下几部分:

  1. 内部值:实例存储了传递给构造函数的初始值。

  2. 依赖收集:实例需要跟踪所有依赖于它的效果(effect),例如计算属性或者副作用函数。这通常通过一个依赖列表或者集合来实现。

  3. 触发更新:当实例的值发生变化时,它需要通知所有依赖于它的效果,以便它们可以重新计算或执行。

  ref 和 reactive 尽管两者在内部实现上有所不同,但它们都能满足我们对于声明响应式变量的要求,但是 reactive 却存在一定的局限性,尤其是对于喜欢使用解构赋值的开发者而言。这些局限性可能会导致意外的行为,因此在使用 reactive 时需要格外注意。相比之下,ref API 提供了一种更灵活和统一的方式来处理响应式数据

        ref() 为响应式编程提供了一种统一的解决方案,适用于所有类型的数据,包括基本数据类型和复杂对象

const age = ref(10)
// 修改时
age.value = 11

const ageArr = ref([10,20])
// 修改时
ageArr.value.push(30)

const boy = ref({name: 'xiaoming',age: 10})
// 修改时
boy.value.age = 11

更多推荐