Skip to content

provide / inject

provide 和 inject 为组件树之间的不同层级提供了一种通信机制,它允许一个祖先组件提供一个可被其所有后代组件使用的值,而不必将这个值通过 Props 逐级传递下去

一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。 image.png

Provide(提供依赖)

Provide() 函数,为后代组件提供数据

typescript
<script setup>
import { provide } from 'vue'

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

如果不使用setup,需要保证 provide() 是在setup()同步调用的:

typescript
import { provide } from 'vue'

export default {
  setup() {
    provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
  }
}

同步调用在这里的含义是指 provide 函数调用必须直接发生在 setup 函数的执行流程中,而不是在任何异步操作(如 setTimeout、Promise.then 或 async 函数等)的回调中

因为Vue的 setup 函数在组件初始化时同步执行,需要确保provide 提供的状态是可以马上供其子组件 inject 依赖的

Error示例

typescript
import { provide } from 'vue';

export default {
  setup() {
    // 错误示范:将 provide 放在异步操作中
    setTimeout(() => {
      provide('message', 'hello!'); // 子组件可能无法接收到这个值
    }, 1000);

    // 其他异步逻辑...
  },
};

// 在 provide 被调用之前,
// 子组件的 setup 函数可能已经执行完毕,这时候子组件尝试 inject 的值将不会存在

Inject(注入)

inject() 函数,注入上层组件提供的数据

typescript
<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值.


如果没有使用 <script setup>inject() 需要在 setup() 内同步调用

typescript
import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message')
    return { message }
  }
}

注入默认值

inject 提供了使用默认值的选项,如果在组件树中未找到提供的值,将使用默认值。

typescript
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')

工厂函数创建默认值, 第三个参数true表示默认值应该被当作一个工厂函数

typescript
const value = inject('key', () => new ExpensiveClass(), true)

工厂函数作为默认值时,只有在没有找到提供的值时才会执行,减少不必要的性能开销

Symbol作为键

推荐使用 Symbol 作为 provide/inject 的键,这样可以确保键的唯一性,避免字符串键可能出现的命名冲突

具体示例

Provide(key, value) 第一个参数: 注入的键名, string | Symbol 第二个参数:给后代提供的值,任意类型响应式ref也ok。

typescript
import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

Inject(key, defaultValue, booleanTrue)

  1. key
    1. 要注入的键名;这是唯一必须提供的参数。这个键必须与祖先组件 provide 时使用的键相对应
  2. defaultValue(可选)
    1. 如果没有找到提供的键,将使用的默认值。
  3. true(可选)
    1. 仅当第二个参数为函数时才有用。如果为 true,将把该函数视为工厂函数并调用它以获取默认值。
    2. 意味着你可以延迟创建默认值,只有在需要时才实例化,避免不必要的性能开销
typescript
<script setup>
import { inject } from 'vue'

// 如果key没有提供,msg => undefined
const msg = inject('key')  
// 如果key没有提供,msgDefault => defaultValue
const msgDefault = inject('key', 'defaultValue')  
// 如果'providedKey'没有提供,将调用 () => new ExpensiveObject() 来获取默认值
const injectedValue = inject('providedKey', () => new ExpensiveObject(), true);
</script>

响应式数据的修改

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中

vue
<!-- 在供给方组件内 -->
<script setup>
  import { provide, ref } from 'vue'

  const location = ref('North Pole')

  function updateLocation() {
    location.value = 'South Pole'
  }

  provide('location', {
    location,
    updateLocation
  })
</script>
vue
<!-- 在注入方组件 -->
<script setup>
  import { inject } from 'vue'

  const { location, updateLocation } = inject('location')
</script>

<template>
  <!-- 相当于子组件调用父组件方法修改父组件传来的数据 -->
  <button @click="updateLocation">{{ location }}</button>
</template>

如果你想确保提供的数据不能被注入方的组件更改,你可以使用readonly()来包装提供的值。

typescript
<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

Symbol作为键名

但如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。

key.ts

推荐在一个单独的文件中导出这些注入名 Symbol:

typescript
// keys.ts
export const myInjectionKey = Symbol()
typescript
// 在供给方组件中
import { provide } from 'vue'
import type { myInjectionKey } from './keys.ts'

provide(myInjectionKey, { /*
  要提供的数据
*/ });
typescript
// 注入方组件
import { inject } from 'vue'
import type { myInjectionKey } from './keys.ts'

const injected = inject(myInjectionKey)