ref 详解

ref 是 Vitarx 中最基础的响应式原语。它将一个值包装为响应式引用,通过 .value 读写值。Vitarx 提供了多种 ref 变体,适用于不同的场景。

ref()

ref() 创建一个深度响应式引用(ValueRef)。当值为对象时,会自动用 reactive 包装,使内部属性也具有响应性。

tsx
import { ref } from 'vitarx'

// 基本类型
const count = ref(0)
count.value = 1

// 对象类型——内部属性自动变为响应式
const user = ref({ name: '张三', age: 25 })
user.value.name = '李四' // 触发更新

.value、.peek 和 .raw

ValueRef 提供了三种读取值的方式:

属性说明是否触发依赖追踪
.value读写响应式值是(读取时追踪,写入时触发)
.peek读取响应式值
.raw读取原始值(未经 reactive 包装)
tsx
import { ref, watchEffect } from 'vitarx'

const count = ref(0)

watchEffect(() => {
  // 访问 .value 会建立依赖关系
  console.log('count 变化了:', count.value)
})

// .peek:读取值但不追踪依赖
// 适合在副作用中读取但不希望被该值的变化触发重新执行
const currentCount = count.peek

// .raw:获取原始值(对象时为未经 reactive 代理的对象)
const obj = ref({ a: 1 })
obj.raw // { a: 1 } —— 普通对象,非代理
obj.peek // Reactive 代理对象
obj.value // Reactive 代理对象(与 peek 相同,但会追踪依赖)

INFO

.peek.raw 都不会触发依赖追踪,区别在于:.peek 返回的是经过 reactive 包装后的代理值,而 .raw 返回的是未经包装的原始值。对于基本类型值,两者结果相同。

shallowRef()

shallowRef() 创建浅层响应式引用(ShallowRef)。只有 .value 的替换才会触发更新,内部属性的变化不会触发。

tsx
import { shallowRef, watchEffect } from 'vitarx'

const list = shallowRef([1, 2, 3])

watchEffect(() => {
  console.log('list 变化了:', list.value)
})

// ❌ 不会触发更新——修改内部元素
list.value.push(4)

// ✅ 会触发更新——替换整个 .value
list.value = [...list.value, 4]

shallowRef 的 .peek 和 .raw

ShallowRef 同样支持 .peek.raw,行为与 ValueRef 一致:

tsx
const data = shallowRef({ count: 0 })

data.peek // { count: 0 } —— 原始对象(未经 reactive 包装)
data.raw // { count: 0 } —— 原始对象
data.value // { count: 0 } —— 同上(ShallowRef 不会 reactive 包装)

何时使用 shallowRef

  • 包装大型数据结构,避免深层代理的性能开销
  • 只关心整体替换,不需要追踪内部属性变化
  • 存储第三方库的实例(如 DOM 元素、Chart 实例),不需要代理其内部

GetterRef(toRef 函数重载)

toRef(() => ...) 创建一个只读引用(GetterRef)。每次访问 .value 时都会调用 getter 函数获取最新值。

tsx
import { reactive, toRef } from 'vitarx'

const state = reactive({ name: '张三', age: 25 })

// 创建只读引用——每次 .value 都会调用 getter
const nameRef = toRef(() => state.name)

console.log(nameRef.value) // '张三'
state.name = '李四'
console.log(nameRef.value) // '李四'

// ❌ 设置 .value 无效——GetterRef 是只读的
nameRef.value = '王五' // 控制台会输出警告

WARNING

GetterRef 是只读的,尝试设置 .value 不会生效,并会在控制台输出警告。

PropertyRef(propertyRef / toRef 对象属性重载)

PropertyRef 用于创建与对象属性双向绑定的引用。修改 .value 会同步修改原对象的属性,反之亦然。

propertyRef()

tsx
import { reactive, propertyRef } from 'vitarx'

const state = reactive({ name: '张三', age: 25 })

// 创建属性引用——双向绑定
const nameRef = propertyRef(state, 'name')

console.log(nameRef.value) // '张三'

// 通过 ref 修改——原对象同步变化
nameRef.value = '李四'
console.log(state.name) // '李四'

// 通过原对象修改——ref 同步变化
state.name = '王五'
console.log(nameRef.value) // '王五'

propertyRef 的默认值

当属性值为 undefined 时,可以使用第三个参数指定默认值:

tsx
import { reactive, propertyRef } from 'vitarx'

const state = reactive({ name: '张三' })

// name 属性不存在,使用默认值
const nameRef = propertyRef(state, 'name', '默认名称')
console.log(nameRef.value) // '张三'(属性有值,不使用默认值)

const nickRef = propertyRef(state, 'nick', '默认昵称')
console.log(nickRef.value) // '默认昵称'(属性为 undefined,使用默认值)

toRef() 多种重载

toRef() 是一个多功能工具函数,根据传入参数的不同,返回不同类型的 ref:

调用方式返回类型说明
toRef(() => value)GetterRef只读引用,通过 getter 获取值
toRef(ref)原 ref传入已有的 ref,原样返回
toRef(普通值)ValueRef将普通值包装为可写的 ref
toRef(obj, key)PropertyRef与对象属性双向绑定
tsx
import { ref, reactive, toRef } from 'vitarx'

// 1. 函数 → GetterRef(只读)
const state = reactive({ count: 0 })
const countRef = toRef(() => state.count)

// 2. 已有 ref → 原样返回
const originalRef = ref(42)
const sameRef = toRef(originalRef)
console.log(originalRef === sameRef) // true

// 3. 普通值 → ValueRef
const numRef = toRef(100)
numRef.value = 200 // 可写

// 4. 对象属性 → PropertyRef(双向绑定)
const nameRef = toRef(state, 'count')
nameRef.value = 10
console.log(state.count) // 10

INFO

toRef(obj, key) 内部使用了缓存机制——对同一个对象的同一个属性多次调用 toRef,返回的是同一个 PropertyRef 实例。

toRefs()

toRefs() 将一个 reactive 对象的所有属性转为 ref 集合,每个 ref 与原属性双向绑定。主要用于解构 reactive 对象时保持响应性。

js
import { reactive, toRefs } from 'vitarx'

const state = reactive({ name: '张三', age: 25 })

// ❌ 直接解构会丢失响应性
const { name, age } = state

// ✅ 使用 toRefs 解构,保持响应性
const { name, age } = toRefs(state)

// name 和 age 都是 PropertyRef,与原属性双向绑定
name.value = '李四'
console.log(state.name) // '李四'

state.age = 30
console.log(age.value) // 30

WARNING

toRefs() 只能用于普通对象,传入非对象会抛出 TypeError。传入非 reactive 对象会在控制台输出警告。

toValue()

toValue() 统一获取值的工具函数——如果参数是 ref 则返回 .value,如果是函数则调用它,否则原样返回。

tsx
import { ref, toValue } from 'vitarx'

const count = ref(0)

toValue(count) // 0 —— ref 取 .value
toValue(() => 42) // 42 —— 函数调用
toValue('hello') // 'hello' —— 普通值原样返回
toValue(100) // 100

这个函数在编写通用逻辑时很有用,可以同时接受 ref、getter 函数或普通值作为参数。

unref()

unref()toValue() 的简化版——如果参数是 ref 则返回 .value,否则原样返回。不支持函数调用。

tsx
import { ref, unref } from 'vitarx'

const count = ref(0)

unref(count) // 0
unref(100) // 100
unref('hello') // 'hello'

INFO

unref()toValue() 的区别:toValue() 额外支持函数参数,会调用函数获取返回值;unref() 只处理 ref 和普通值。

isRef() 和 isRefSignal()

isRef() 判断一个值是否实现了 Ref 接口(拥有 [IS_REF] 标识和 value 属性)。isRefSignal() 在此基础上进一步判断是否为 RefSignal(拥有 [IS_SIGNAL] 标识)。

tsx
import { ref, shallowRef, toRef, computed, isRef, isRefSignal } from 'vitarx'

const count = ref(0)
const list = shallowRef([1, 2])
const getterRef = toRef(() => count.value)
const double = computed(() => count.value * 2)

// isRef:所有 ref 类型都返回 true
isRef(count) // true
isRef(list) // true
isRef(getterRef) // true
isRef(double) // true
isRef(100) // false

// isRefSignal:只有 ValueRef 和 ShallowRef 返回 true
isRefSignal(count) // true  —— ValueRef
isRefSignal(list) // true  —— ShallowRef
isRefSignal(getterRef) // false —— GetterRef 不是 RefSignal
isRefSignal(double) // true  —— Computed 实现了 RefSignal

完整示例

下面是一个综合使用各种 ref API 的示例:

tsx
import { ref, shallowRef, toRef, toRefs, toValue, unref, isRef, reactive, createApp } from 'vitarx'

function App() {
  // ValueRef:深度响应式
  const count = ref(0)

  // ShallowRef:浅层响应式
  const config = shallowRef({ theme: 'light', fontSize: 14 })

  // reactive 对象
  const user = reactive({ name: '张三', age: 25 })

  // toRefs:解构保持响应性
  const { name, age } = toRefs(user)

  // toRef getter:只读引用
  const summary = toRef(() => `#123;name.value},#123;age.value}岁`)

  // toValue:统一获取值
  const getValue = (source) => toValue(source)

  const increment = () => {
    count.value++
  }

  const toggleTheme = () => {
    // shallowRef 需要替换整个 .value
    config.value = {
      ...config.value,
      theme: config.value.theme === 'light' ? 'dark' : 'light'
    }
  }

  return (
    <div style={{ padding: '20px' }}>
      <h2>ref 详解示例</h2>

      <div style={{ marginTop: '16px' }}>
        <h3>ValueRef 计数器</h3>
        <p>当前计数: {count}</p>
        <button onClick={increment}>+1</button>
      </div>

      <div style={{ marginTop: '16px' }}>
        <h3>ShallowRef 配置</h3>
        <p>主题: {config.value.theme}</p>
        <button onClick={toggleTheme}>切换主题</button>
      </div>

      <div style={{ marginTop: '16px' }}>
        <h3>toRefs 解构</h3>
        <p>姓名: {name}</p>
        <p>年龄: {age}</p>
        <button onClick={() => age.value++}>长大一岁</button>
      </div>

      <div style={{ marginTop: '16px' }}>
        <h3>GetterRef 只读引用</h3>
        <p>摘要: {summary}</p>
      </div>
    </div>
  )
}

createApp(App).mount('#app')

下一步

  • reactive 详解 — 深入理解 reactive 的代理机制和集合类型支持