编程 provide 了还报 injection "x" not found:inject 的六个失效点按概率排

2026-10-30 00:04:55

inject 返回 undefined 或值不更新:Vue 3 provide/inject 排查顺序

provide() / inject() 用来绕开 props 逐层传递。父组件作为依赖提供者,任意深度的后代都可以注入。它同时带来两个约束:inject() 只在 setup 同步阶段有效,找不到 key 时不会报错中断,只给运行时警告或返回 undefined。下面按发生概率排一遍排查顺序,再逐条说机制和修法。

排查顺序

  1. 当前组件上方根本没有 provider,或 provider 在组件树之外
  2. 提供的值是 .value 或 reactive(ref),响应式链断掉
  3. provide 和 inject 从不同的包导入
  4. 调用时机不在 setup 同步阶段,或存在循环依赖
  5. key 对不上、类型是 unknown
  6. 单元测试里没有注入
  7. 只想在库内部用 inject(),却不该给终端用户报警告

1. 没有祖先提供者

inject() 沿父链向上查找匹配 key 的值,多个父组件提供同一 key 时,最近的会遮蔽更上层的。找不到就返回 undefined,除非给了默认值。没有默认值时控制台出现:

[Vue warn]: Injection "theme" not found

典型触发场景是组件被单独使用:封装了 CheckboxGroup,父组件 provide('CheckboxGroup', proxy),子组件 inject('CheckboxGroup'),一旦把子组件放到没有父组件的页面就告警。修法是给默认值:

const test = inject('test', () => {})
const checkboxGroup = inject('CheckboxGroup', null)

如果这个值本来就该全局可见(写插件时尤其常见,插件通常不能用组件形式提供值),用应用级注入:

app.provide('theme', 'dark')

它对应用内所有组件可见,不依赖组件树位置。

2. 响应性丢失

inject() 拿到的是提供出去的 ref 本身,不会自动解包,这样注入方才能通过 ref 保持与供给方的响应式链接。所以提供时必须给 ref,而不是 .value,也不是把 ref 塞进 reactive():

// ❌ reactive(ref) 包了一层,注入方拿到的不是那个 ref
const returnedWeatherData = reactive(weatherData)
provide('returnedWeatherData', returnedWeatherData)

// ✅ 直接提供 ref
provide('returnedWeatherData', weatherData)

另一个容易误判的现象:子组件 setup 里的 console.log 不会因为值变化而重新执行,需要包在 watchEffect 里才会在 ref 变化时再次触发。日志没刷新不代表注入没生效。

选项式 API 里同样有坑:provide() { return { message: computed(() => this.message) } } 是响应式的,直接返回 this.message 不是。

模板解包在两种 API 下曾不一致:选项式 provide: { user: computed(() => 'jim') } + inject: ['user'],模板里必须写 {{ user.value }},而 Composition API 不需要。yyx990803 的回复是解包应当一致,但这属于破坏性变更,折中做法是先给警告并要求显式选择,之后的小版本再默认修正。

变更尽量留在供给方,提供修改函数而不是把裸 ref 交出去;需要禁止注入方写入时,提供只读副本:

provide('location', { location, updateLocation })
provide('read-only-count', readonly(count))

3. 从不同的包导入

inject 从 @vue/runtime-core 导入、provide 从 vue 导入,两边 key 和上下文对不上,注入会失败。统一从 vue 导入即可。这条排查起来最快,先看一眼 import 语句。

4. 调用时机与循环依赖

provide() 和 inject() 都必须在组件 setup() 阶段同步调用,和生命周期钩子注册 API 类似。不在 `` 里时,要确保在 setup() 内同步执行;放到 await 之后或回调里就失效。

循环依赖的表现是 A 提供、B 注入、B 又反过来影响 A 的初始化。做法是延迟初始化:同步 provide 一个 ref,在 onMounted 里再填充它的值。

5. key 与类型

官方签名:

function provide(key: InjectionKey | string, value: T): void

// 无默认值
function inject(key: InjectionKey | string): T | undefined
// 带默认值
function inject(key: InjectionKey | string, defaultValue: T): T
// 带工厂函数
function inject(key: InjectionKey | string, defaultValue: () => T, treatDefaultAsFactory: true): T

key 可以是字符串或 Symbol,一个组件可以用不同 key 多次 provide()。字符串 key 在大应用里容易撞名,组件库更建议用 Symbol,并在单独文件导出:

export const myInjectionKey = Symbol()

TypeScript 下用 InjectionKey 在 provide 与 inject 之间同步类型。类型显示为 unknown 基本就是没用它:

import type { InjectionKey, Ref } from 'vue'

const ThemeKey: InjectionKey> = Symbol('theme')

默认值创建开销大时,第二参数传工厂函数、第三参数传 true,避免用不到时也计算或产生副作用:

inject('key', () => new ExpensiveClass(), true)

6. 测试时没有注入

mount 时通过 global.provide 补上,key 为 Symbol 时注意类型转换:

mount(MyComponent, {
global: {
provide: { [ThemeKey as symbol]: ref('dark') }
}
})

调试线上问题时,getCurrentInstance().provides 可以查看祖先链到底提供了什么;Vue DevTools 的 Provided / Injected 面板也能直接对照。

7. 库作者:hasInjectionContext

hasInjectionContext() 在 3.3+ 提供,返回 true 表示 inject() 可以在不触发「调用位置错误」警告的情况下使用。它是为库作者设计的,让库内部调用 inject() 时不必打扰终端用户。

什么时候不该用 provide/inject

provide/inject 与组件树绑定:组件被移到 provider 之外就失效,路由切走状态即消失,而且只对后代生效,兄弟组件传不了。

Pinia 是单例:同一组件在一页内复用多次时状态会互相污染,这种场景只能靠 provide/inject 做实例级隔离;反过来,组件卸载了 store 还在,需要手动 reset;什么都往里塞会变成大泥球。

判断标准:只服务当前组件树、路由走就该清空,用 provide/inject;要被别的路由、侧边栏或导出读到,或要同步 URL、localStorage、看 devtools 历史,用 Pinia。

第三条路是把筛选条件同步到 route.query,刷新不丢、可分享、后退可用,代价是频繁输入要防抖。

最后一点与本文主题无关但常一起踩:Pinia 里 const { status } = useFilterStore() 一样丢响应性,要 storeToRefs。这是响应式框架的通病,不是 provide/inject 专属。

推荐文章

程序员茄子在线接单