Skip to content

响应式 API

ref()

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

  • 类型

    ts
    function ref<T>(value: T): Ref<UnwrapRef<T>>;
    
    interface Ref<T> {
      value: T;
    }
  • 详细信息

    ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

    如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

    若要避免这种深层次的转换,请使用 shallowRef() 来替代。

  • 示例

    js
    const count = ref(0);
    console.log(count.value); // 0
    
    count.value = 1;
    console.log(count.value); // 1

isRef()

检查某个值是否为 ref。

  • 类型

    ts
    function isRef<T>(r: Ref<T> | unknown): r is Ref<T>;

    请注意,返回值是一个类型判定 (type predicate),这意味着 isRef 可以被用作类型守卫:

    ts
    let foo: unknown;
    if (isRef(foo)) {
      // foo 的类型被收窄为了 Ref<unknown>
      foo.value;
    }

unref()

如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

  • 类型

    ts
    function unref<T>(ref: T | Ref<T>): T;
  • 示例

    ts
    function useFoo(x: number | Ref<number>) {
      const unwrapped = unref(x);
      // unwrapped 现在保证为 number 类型
    }

toRef()

可以将值、refs 或 getters 规范化为 refs (3.3+)。

也可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

  • 类型

    ts
    // 规范化签名 (3.3+)
    function toRef<T>(value: T): T extends () => infer R ? Readonly<Ref<R>> : T extends Ref ? T : Ref<UnwrapRef<T>>;
    
    // 对象属性签名
    function toRef<T extends object, K extends keyof T>(object: T, key: K, defaultValue?: T[K]): ToRef<T[K]>;
    
    type ToRef<T> = T extends Ref ? T : Ref<T>;
  • 示例

    规范化签名 (3.3+):

    js
    // 按原样返回现有的 ref
    toRef(existingRef);
    
    // 创建一个只读的 ref,当访问 .value 时会调用此 getter 函数
    toRef(() => props.foo);
    
    // 从非函数的值中创建普通的 ref
    // 等同于 ref(1)
    toRef(1);

    对象属性签名:

    js
    const state = reactive({
      foo: 1,
      bar: 2,
    });
    
    // 双向 ref,会与源属性同步
    const fooRef = toRef(state, 'foo');
    
    // 更改该 ref 会更新源属性
    fooRef.value++;
    console.log(state.foo); // 2
    
    // 更改源属性也会更新该 ref
    state.foo++;
    console.log(fooRef.value); // 3

    请注意,这不同于:

    js
    const fooRef = ref(state.foo);

    上面这个 ref 不会state.foo 保持同步,因为这个 ref() 接收到的是一个纯数值。

    toRef() 这个函数在你想把一个 prop 的 ref 传递给一个组合式函数时会很有用:

    vue
    <script setup>
    import { toRef } from 'vue';
    
    const props = defineProps(/* ... */);
    
    // 将 `props.foo` 转换为 ref,然后传入
    // 一个组合式函数
    useSomeFeature(toRef(props, 'foo'));
    
    // getter 语法——推荐在 3.3+ 版本使用
    useSomeFeature(toRef(() => props.foo));
    </script>

    toRef 与组件 props 结合使用时,关于禁止对 props 做出更改的限制依然有效。尝试将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。在这种场景下,你可能可以考虑使用带有 getsetcomputed 替代。

    当使用对象属性签名时,即使源属性当前不存在,toRef() 也会返回一个可用的 ref。这让它在处理可选 props 的时候格外实用,相比之下 toRefs 就不会为可选 props 创建对应的 refs。

toRefs()

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

  • 类型

    ts
    function toRefs<T extends object>(
      object: T,
    ): {
      [K in keyof T]: ToRef<T[K]>;
    };
    
    type ToRef = T extends Ref ? T : Ref<T>;
  • 示例

    js
    const state = reactive({
      foo: 1,
      bar: 2,
    });
    
    const stateAsRefs = toRefs(state);
    /*
    stateAsRefs 的类型:{
      foo: Ref<number>,
      bar: Ref<number>
    }
    */
    
    // 这个 ref 和源属性已经“链接上了”
    state.foo++;
    console.log(stateAsRefs.foo.value); // 2
    
    stateAsRefs.foo.value++;
    console.log(state.foo); // 3

    当从组合式函数中返回响应式对象时,toRefs 相当有用。使用它,消费者组件可以解构/展开返回的对象而不会失去响应性:

    js
    function useFeatureX() {
      const state = reactive({
        foo: 1,
        bar: 2,
      });
    
      // ...基于状态的操作逻辑
    
      // 在返回时都转为 ref
      return toRefs(state);
    }
    
    // 可以解构而不会失去响应性
    const { foo, bar } = useFeatureX();

    toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef

toValue()

将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。

这可以在组合式函数中使用,用来规范化一个可以是值、ref 或 getter 的参数。

  • 类型

    ts
    function toValue<T>(source: T | Ref<T> | (() => T)): T;
  • 示例

    js
    toValue(1); //       --> 1
    toValue(ref(1)); //  --> 1
    toValue(() => 1); // --> 1

    在组合式函数中规范化参数:

    ts
    import type { MaybeRefOrGetter } from 'vue';
    
    function useFeature(id: MaybeRefOrGetter<number>) {
      watch(
        () => toValue(id),
        id => {
          // 处理 id 变更
        },
      );
    }
    
    // 这个组合式函数支持以下的任意形式:
    useFeature(1);
    useFeature(ref(1));
    useFeature(() => 1);

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()

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

  • 类型

    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();
            return value;
          },
          set(newValue) {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
              value = newValue;
              trigger();
            }, delay);
          },
        };
      });
    }

    在组件中使用:

    vue
    <script setup>
    import { useDebouncedRef } from './debouncedRef';
    const text = useDebouncedRef('hello');
    </script>
    
    <template>
      <input v-model="text" />
    </template>

reactive()

返回一个对象的响应式代理。

  • 类型

    ts
    function reactive<T extends object>(target: T): UnwrapNestedRefs<T>;
  • 详细信息

    响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。

    值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

    若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。

    返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。

  • 示例

    创建一个响应式对象:

    js
    const obj = reactive({ count: 0 });
    obj.count++;

    ref 的解包:

    ts
    const count = ref(1);
    const obj = reactive({ count });
    
    // ref 会被解包
    console.log(obj.count === count.value); // true
    
    // 会更新 `obj.count`
    count.value++;
    console.log(count.value); // 2
    console.log(obj.count); // 2
    
    // 也会更新 `count` ref
    obj.count++;
    console.log(obj.count); // 3
    console.log(count.value); // 3

    注意当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包:

    js
    const books = reactive([ref('Vue 3 Guide')]);
    // 这里需要 .value
    console.log(books[0].value);
    
    const map = reactive(new Map([['count', ref(0)]]));
    // 这里需要 .value
    console.log(map.get('count').value);

    将一个 ref 赋值给一个 reactive 属性时,该 ref 会被自动解包:

    ts
    const count = ref(1);
    const obj = reactive({});
    
    obj.count = count;
    
    console.log(obj.count); // 1
    console.log(obj.count === count.value); // true

isReactive()

检查一个对象是否是由 reactive()shallowReactive() 创建的代理。

  • 类型

    ts
    function isReactive(value: unknown): boolean;

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++;

readonly()

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。

  • 类型

    ts
    function readonly<T extends object>(target: T): DeepReadonly<UnwrapNestedRefs<T>>;
  • 详细信息

    只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。

    要避免深层级的转换行为,请使用 shallowReadonly() 作替代。

  • 示例

    js
    const original = reactive({ count: 0 });
    
    const copy = readonly(original);
    
    watchEffect(() => {
      // 用来做响应性追踪
      console.log(copy.count);
    });
    
    // 更改源属性会触发其依赖的侦听器
    original.count++;
    
    // 更改该只读副本将会失败,并会得到一个警告
    copy.count++; // warning!

isReadonly()

检查传入的值是否为只读对象。只读对象的属性可以更改,但他们不能通过传入的对象直接赋值。

通过 readonly()shallowReadonly() 创建的代理都是只读的,因为他们是没有 set 函数的 computed() ref。

  • 类型

    ts
    function isReadonly(value: unknown): boolean;

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++;

isProxy()

检查一个对象是否是由 reactive()readonly()shallowReactive()shallowReadonly() 创建的代理。

  • 类型

    ts
    function isProxy(value: unknown): boolean;

toRaw()

根据一个 Vue 创建的代理返回其原始对象。

  • 类型

    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 作用域中调用的。

  • 类型

    ts
    function onScopeDispose(fn: () => void): void;

computed()

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

watch()

watchEffect()

watchPostEffect()

watchSyncEffect()

Released Under The MIT License.