跳到主要内容

ref

在 Vue 3 的 Composition API 中,ref 是一个非常重要的函数,用于创建一个响应式的引用对象。这个引用对象可以包裹任何类型的值,包括基本类型(如 String、Number)或对象类型。使用 ref 创建的响应式数据,Vue 会自动跟踪它们的变化,并在变化时更新视图。这为在 Vue 3 中管理状态提供了极大的灵活性和便利。

基本用法

import { ref } from 'vue';

const count = ref(0); // 创建一个响应式的引用,初始值为 0

在模板中直接使用时,Vue 会自动解包 ref 对象

<template>
<div>{{ count }}</div> <!-- Vue 自动解包 `count` 的值 -->
</template>

在 JavaScript 中访问或修改 ref 创建的响应式引用的值时,需要通过 .value 属性:

console.log(count.value); // 读取 `count` 的值
count.value++; // 修改 `count` 的值

为什么 ref 要设计成通过 .value 访问呢

  1. 统一基本类型和引用类型的响应式包装

Vue 3 的响应式系统使用 Proxy 来实现,而 Proxy 可以代理对象,但不能直接代理基本类型(如字符串、数字等)。为了让基本类型也能享有响应式的特性,Vue 通过 ref 提供一个对象包装器,这个对象有一个 .value 属性指向实际的数据。这样,无论是基本类型还是引用类型,都可以统一通过 .value 来访问和修改,使得 Vue 的响应式系统更加灵活和通用。

  1. 保持响应式引用的一致性

在 Vue 3 中,使用 ref 和 reactive 都可以创建响应式数据,但它们的使用场景和目的有所不同。ref 主要用于基本类型和需要被包装为响应式的单一值,而 reactive 用于对象和数组。通过要求访问 .value,ref 保持了对所有类型值的一致访问方式,无论它们是否被 reactive 处理。这种设计简化了 API 并减少了使用上的混淆。

  1. 自动解包在模板中的便利性

虽然在 JavaScript 中需要通过 .value 来访问 ref 包装的数据,Vue 3 的模板编译器会自动解包模板中的 ref,使得在模板中使用时无需指定 .value。这种自动解包的特性提供了模板表达式的便利性,同时保留了在 JavaScript 中显式访问控制的能力。

  1. 显式性和可预测性

通过 .value 访问强制开发者显式地处理响应式引用,这增加了代码的可读性和可维护性。开发者可以清楚地看到哪些变量是响应式的,并且需要通过 .value 进行访问或修改,这种显式性有助于防止错误的使用,并使得代码的意图更加明确。

ref 与 reactive 的区别

  • ref 用于创建一个包含单个值的响应式引用。它可以用于基本类型值,也可以用于对象。对于基本类型值,ref 提供了一种方法来使它们变得响应式。
    • 适用场景:ref 适合管理单一的数据项,特别是基本类型的数据,也可以用于对象和数组,但对于复杂的嵌套对象,reactive 可能是更好的选择。
  • reactive 用于创建响应式的对象或数组。它直接接受一个对象或数组并返回其响应式代理。
    • 不适用于基本类型:reactive 不能直接用于基本类型数据。尝试对基本类型使用 reactive 将不会使其变为响应式。
    • 适用场景:reactive 适合用于对象和数组的响应式转换,特别是需要管理多个相关数据或嵌套数据时。
import { ref, reactive } from 'vue';

// 使用 ref
const count = ref(0); // 基本类型
const userProfile = ref({ name: 'John Doe', age: 30 }); // 对象

// 使用 reactive
const userState = reactive({ name: 'John Doe', age: 30, likes: ['reading', 'hiking'] }); // 对象

源码示例

源码地址: https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts#L152



function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}

export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value

class RefImpl<T> {
private _value: T
private _rawValue: T

public dep?: Dep = undefined
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)
}

get value() {
trackRefValue(this)
return this._value
}

set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}

/**
* Force trigger effects that depends on a shallow ref. This is typically used
* after making deep mutations to the inner value of a shallow ref.
*
* @example
* ```js
* const shallow = shallowRef({
* greet: 'Hello, world'
* })
*
* // Logs "Hello, world" once for the first run-through
* watchEffect(() => {
* console.log(shallow.value.greet)
* })
*
* // This won't trigger the effect because the ref is shallow
* shallow.value.greet = 'Hello, universe'
*
* // Logs "Hello, universe"
* triggerRef(shallow)
* ```
*
* @param ref - The ref whose tied effects shall be executed.
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/
export function triggerRef(ref: Ref) {
triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0)
}