Skip to content

进阶响应式

shallowRef()

ref() 的浅层作用形式。只处理基本数据类型的响应式, 不进行对象的响应式处理。

类型

ts
function shallowRef<T>(value: T): ShallowRef<T>

interface ShallowRef<T> {
  value: T
}

详细信息

ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

示例

js
const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }

triggerRef()

强制触发依赖于一个shallowRef()的副作用,这通常在对浅引用的内部值进行深度变更后使用。

类型

ts
function triggerRef(ref: ShallowRef): void

示例

js
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这次变更不应触发副作用,因为这个 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 打印 "Hello, universe"
triggerRef(shallow)

customRef()

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

类型

ts
function customRef<T>(factory: CustomRefFactory<T>): Ref<T>

type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

详细信息

customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。

示例

创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:

js
import { customRef } from 'vue'

export function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()		//告诉Vue这个value值是需要被“追踪”的
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()			//告诉Vue去更新界面
        }, delay)
      }
    }
  })
}

在组件中使用:

vue
<script setup>
import { useDebouncedRef } from './debouncedRef'
const text = useDebouncedRef('hello')
</script>

<template>
  <input v-model="text" />
</template>

谨慎使用

当使用 customRef 时,我们应该谨慎对待其 getter 的返回值,尤其是在每次运行 getter 时都生成新对象数据类型的情况下。当这样的 customRef 作为 prop 传递时,将影响父组件和子组件之间的关系。

父组件的渲染函数可能会被其他的响应式状态变化触发。在重新渲染过程中,我们会重新评估 customRef 的值,并返回一个新的对象数据类型作为子组件的 prop。这个 prop 会与其上一个值进行比较,由于两者不同,子组件中 customRef 的响应式依赖将被触发。与此同时,因为没有调用 customRef 的 setter,父组件中的响应式依赖不会运行。

shallowReactive()

reactive() 的浅层作用形式。只处理对象最外层属性的响应式

类型

ts
function shallowReactive<T extends object>(target: T): T

详细信息

reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

谨慎使用

浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。

示例

js
const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 更改状态自身的属性是响应式的
state.foo++

// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false

// 不是响应式的
state.nested.bar++

shallowReadonly()

readonly()的浅层作用形式

类型

ts
function shallowReadonly<T extends object>(target: T): Readonly<T>

详细信息

readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

谨慎使用

浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。

示例

js
const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 更改状态自身的属性会失败
state.foo++

// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false

// 这是可以通过的
state.nested.bar++

toRaw()

根据一个 Vue 创建的代理返回其原始对象。将一个由reactive生成的响应式对象转为普通对象。

用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。

类型

ts
function toRaw<T>(proxy: T): T

详细信息

toRaw() 可以返回由 reactive()readonly()shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。

这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

示例

js
const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

markRaw()

标记一个对象,使其永远不会再成为响应式对象。返回该对象本身。

类型

ts
function markRaw<T extends object>(value: T): T

示例

js
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

谨慎使用

markRaw() 和类似 shallowReactive() 这样的浅层式 API 使你可以有选择地避开默认的深度响应/只读转换,并在状态关系谱中嵌入原始的、非代理的对象。它们可能出于各种各样的原因被使用:

  • 有些值不应该是响应式的,例如复杂的第三方类实例或 Vue 组件对象。
  • 当呈现带有不可变数据源的大型列表时,跳过代理转换可以提高性能。

这应该是一种进阶需求,因为只在根层能访问到原始值,所以如果把一个嵌套的、没有标记的原始对象设置成一个响应式对象,然后再次访问它,你获取到的是代理的版本。这可能会导致对象身份风险,即执行一个依赖于对象身份的操作,但却同时使用了同一对象的原始版本和代理版本:

js
const foo = markRaw({
  nested: {}
})

const bar = reactive({
  // 尽管 `foo` 被标记为了原始对象,但 foo.nested 却没有
  nested: foo.nested
})

console.log(foo.nested === bar.nested) // false

识别风险一般是很罕见的。然而,要正确使用这些 API,同时安全地避免这样的风险,需要你对响应性系统的工作方式有充分的了解。

effectScope()

创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。对于该 API 的使用细节,请查阅对应的 RFC

类型

ts
function effectScope(detached?: boolean): EffectScope

interface EffectScope {
  run<T>(fn: () => T): T | undefined // 如果作用域不活跃就为 undefined
  stop(): void
}

示例

js
const scope = effectScope()

scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(doubled.value))

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 处理掉当前作用域内的所有 effect
scope.stop()

getCurrentScope()

如果有的话,返回当前活跃的 effect 作用域

类型

ts
function getCurrentScope(): EffectScope | undefined

onScopeDispose()

在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。

这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的。

如果在没有活跃的 effect 作用域的情况下调用此函数,将会抛出警告。在 3.5+ 版本中,可以通过将第二个参数设为 true 来消除此警告。

类型

ts
function onScopeDispose(fn: () => void, failSilently?: boolean): void

MIT Licensed