Vue3: 深入学习Ref和Reactive

411 阅读5分钟

在Vue 3中,Ref和Reactive是新的响应式API。它们是在Vue 2中的数据绑定的基础上进一步发展的,提供了更好的性能和更好的开发体验。

Ref

Ref是一个用于创建具有响应性的简单值的函数。它将任何类型的值转换为具有.value属性的响应式对象,该属性可以读取和修改该值。

使用方式

创建Ref对象

import { ref } from 'vue'
const name = ref('Allen')

访问属性

// 要访问这个值, 可以使用.value属性
console.log(name.value) // Allen

修改属性

name.value = "Alan"
console.log(name.value) // Alan

源码解析

Ref的实现方式很简单,它实际上只是一个包含value属性的JavaScript对象。当Ref对象的value属性被读取或设置时,它将触发getter或setter函数。这是通过使用Proxy对象来实现的。在Ref函数中,Vue3使用了以下代码来创建一个Ref对象:

function ref(value) {
  return createRef(value)
}

function createRef(rawValue, shallow = false) {
  // 使用一个对象来包装原始值,这个对象有一个 value 属性
  const r = {
    _isRef: true,
    value: shallow ? rawValue : convert(rawValue)
  }
  // 然后创建一个代理对象,用于观察 value 属性的访问和修改
  return new Proxy(r, {
    get(target, key, receiver) {
      // 如果访问 value 属性,则返回它的值
      if (key === 'value') {
        trackRefValue(target)
        return target._value
      }
      // 其他情况,直接从对象本身获取属性
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      // 如果修改 value 属性,则更新它的值并通知更新
      if (key === 'value') {
        if (value !== target._value) {
          target._value = shallow ? value : convert(value)
          triggerRefValue(target)
        }
        return true
      }
      // 其他情况,直接从对象本身设置属性
      return Reflect.set(target, key, value, receiver)
    }
  })
}

Reactive

Reactive是一个用于创建具有响应性的对象的函数。它将对象转换为一个代理对象,该代理对象具有与原始对象相同的属性和方法,但是这些属性和方法都具有响应性。

使用方式

创建Reactive对象

import { reactive } from 'vue'

const person = reactive({
  name: 'Allen',
  age: 28
})

访问Reactive对象

console.log(person.name) // Allen

修改属性

person.age = 30
console.log(person.age) // 30

源码解析

实现方式与Ref对象类似。它们都使用Proxy对象来实现响应式。但是,Reactive对象比Ref对象更复杂,因为它需要处理嵌套的对象。

function reactive(target) {
  // 如果 target 已经是响应式对象,则直接返回它
  if (target && target._isReactive) {
    return target
  }
 
  // 如果 target 已经被包装,则使用原始对象
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
 
  // 创建一个新的响应式代理对象
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      track(target, TrackOpTypes.GET, key)
      return isObject(res) ? reactive(res) : res
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      // 首先更新值
      const result = Reflect.set(target, key, value, receiver)
      // 如果新值和旧值相同,则不需要触发更新
      if (result && oldValue !== value) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
      return result
    },
    deleteProperty(target, key) {
      const hasKey = hasOwn(target, key)
      const oldValue = target[key]
      const result = Reflect.deleteProperty(target, key)
      if (result && hasKey) {
        trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
      }
      return result
    }
  })
 
  // 把包装后的对象标记为已经响应式,并存储到 reactiveMap 中 
  markRaw(target) 
  reactiveMap.set(target, proxy)
 
  return proxy
}

总结

  • Ref用于包装单个值,而Reactive用于包装对象。
  • Ref返回一个包含.value属性的对象,而Reactive返回一个代理对象,该代理对象具有与原始对象相同的属性和方法,但是这些属性和方法都具有响应性。
  • 在使用Ref时,您需要使用.value属性访问值或修改值。而在Reactive中,您可以像操作普通JavaScript对象一样访问和修改对象的属性。
  • Ref可以更轻松地与基于模板的Vue组件一起使用,而Reactive则更适用于基于函数的组件或独立的JavaScript代码。

defineProperty API 的不足

  1. 不能监听新增或删除属性:Object.defineProperty只能拦截已经存在的属性的读取和设置操作,不能监听新增或删除属性的操作。在实际使用时可通过Vue.set方式操作属性。
  2. 只能监听对象属性的变化,不能监听数组的变化:虽然可以使用Object.defineProperty监听对象属性的变化,但是它无法直接监听数组的变化。在Vue2.x中,需要使用hack的方法来监听数组的变化。可以通过重写数组的pushshift等方法触发更新
  3. 性能问题:由于Object.defineProperty只能逐个属性地监听对象的变化,所以对于大型对象来说,会导致性能问题。Vue2引入了watcher机制。watcher会记录哪些组件依赖于哪些数据,只有当数据发生变化时才会更新这些组件。
  4. 复杂性:使用Object.defineProperty实现响应式需要写很多代码,并且需要非常深入地了解JavaScript语言的特性,使得代码难以维护和理解。

为什么要用 Proxy API 替代 defineProperty API?

  1. 更好的性能表现:使用Proxy API实现响应式,相比于Object.defineProperty实现响应式,有更好的性能表现。这是因为Proxy可以直接代理整个对象,而Object.defineProperty只能一个属性一个属性地进行代理,因此在处理大型对象时Proxy更为高效。
  2. 更好的语法支持:使用Proxy API可以提供更自然的语法支持,使得开发者在使用响应式API时可以使用与普通JavaScript代码更为相似的语法。相比之下,使用Object.defineProperty需要更多的代码,并且需要更深入地了解JavaScript语言特性。
  3. 更丰富的代理能力:Proxy API支持拦截的操作更多,包括读取、设置、删除、枚举等等,而Object.defineProperty只能拦截读取和设置操作。
  4. 更好的错误提示:使用Proxy API在出错时可以提供更好的错误提示,帮助开发者更快地发现和解决问题。