hasInjectionContext() 判的只是 currentInstance 与 currentApp:四个边界与一条自查清单
Vue 3.3 加了 hasInjectionContext(),库作者用它包住内部的 inject(),避免污染终端用户的控制台。但它的返回值并不等于「注入能成功」,也不等于「provide 能用」。下面从警告现象、源码判定条件、四个失效边界依次拆开,最后给一份可以直接照着查的判据。
两种 inject 警告不是一回事
开发环境下控制台里会出现两种来源完全不同的警告,经常被混为一谈:
[Vue warn]: inject() can only be used inside setup() or functional components.—— 调用位置没有注入上下文。[Vue warn]: injection "Symbol(router)" not found.—— 有上下文,但没人 provide 这个 key。
第一条是位置问题,第二条是数据问题。hasInjectionContext() 只处理第一条。vue-router、pinia、vueuse、TanStack Query 这类库在内部调 inject() 前先做守卫,目的就是让第一条不出现;第二条仍然要靠传默认值来消。
源码里的判定条件
packages/runtime-core/src/apiInject.ts(源码):
/**
* Returns true if `inject()` can be used without warning about being called in the wrong place (e.g. outside of
* setup()). This is used by libraries that want to use `inject()` internally without triggering a warning to the end
* user. One example is `useRoute()` in `vue-router`.
*/
export function hasInjectionContext(): boolean {
return !!(getCurrentInstance() || currentApp)
}
packages/runtime-core/src/component.ts:
export let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance || currentRenderingInstance
export const unsetCurrentInstance = (): void => {
currentInstance && currentInstance.scope.off()
internalSetCurrentInstance(null)
}
packages/runtime-core/src/apiCreateApp.ts(源码):
runWithContext(fn) {
const lastApp = currentApp
currentApp = app
try {
return fn()
} finally {
currentApp = lastApp
}
}
把这三段拼起来,结论很窄:hasInjectionContext() 只回答一个问题——当前是否存在 currentInstance(或正在渲染的实例),或者是否存在 currentApp。它完全不看 provides 里有没有对应的 key。
对比一下 provide() 的判定,注意它用的是模块变量 currentInstance,不是 getCurrentInstance():
export function provide(key: K, value: ...): void {
if (__DEV__) {
if (!currentInstance || currentInstance.isMounted) {
warn(`provide() can only be used inside setup().`)
}
}
if (currentInstance) {
let provides = currentInstance.provides
const parentProvides = currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
provides[key as string] = value
}
}
inject() 里挑选 provides 对象的优先级则是:
let provides = currentApp
? currentApp._context.provides
: instance
? instance.parent == null || instance.ce
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: undefined
if (provides && (key as string | symbol) in provides) {
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
「读」认 getCurrentInstance() 加 currentApp,「写」只认 currentInstance。这条不对称是后面四个边界的根源。
边界 1:await 之后返回 false
component.ts 里对 async setup 的处理:
if (isAsyncSetup) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
...
}
异步 setup 的同步部分跑完,currentInstance 就被置回 null。所以在 await 之后的任何一行里调 hasInjectionContext(),拿到的都是 false,inject() 走无上下文分支,被库的守卫跳过,最终返回 fallback 或直接抛错。
TanStack Query 引入该能力的 PR #5886 评论区里,用户 callumacrae 记录了这个坑:await 之后注入上下文就不存在了,库会抛错;变通做法是在 async 逻辑之前先把 queryClient 取出来,再作为参数传进 useQuery:
const queryClient = useQueryClient()
const { isClient } = useCurrentOrg()
if (!(await isClient())) {
next('/')
return
}
const { trial, isTrialEnded } = useTrial({}, queryClient)
边界 2:app.runWithContext() 里 hasInjectionContext() 为 true,但 provide() 无效
runWithContext 只替换 currentApp,不碰 currentInstance。于是同一个回调里:
hasInjectionContext()→true(currentApp非空)inject()可用,provides 取自currentApp._context.provides,也就是app.provide()注册的那些provide()不生效:currentInstance为null,dev 下照旧 warnprovide() can only be used inside setup().,写入分支也被if (currentInstance)挡掉
所以 runWithContext 是个只读的注入上下文。在插件里、路由守卫里、单元测试里想给下游临时塞一个值,它帮不上忙;临时值只能通过 app.provide() 或显式传参。
边界 3:函数式组件渲染期间 hasInjectionContext() 为 true,provide() 同样 warn
getCurrentInstance() 会 fallback 到 currentRenderingInstance,注释写得很直白——为了方便在函数式组件里调用 inject。因此渲染期间 hasInjectionContext() 是 true。但 provide() 的 dev 判定用的是模块变量 currentInstance,渲染期间它是 null,警告照出,写入也不发生。
对库来说这通常无所谓,库只需要读。但如果在函数式组件里想 provide 一个值给子树,这就是无效代码,而且除了那条 dev 警告之外没有任何反馈。想 provide,只有组件 setup 同步段和 app.provide() 两个位置。
边界 4:key 没被 provide 时 inject 仍然 warn
hasInjectionContext() 只消掉「错误位置」那一条。上下文存在、key 不在 provides 里、又没传第二个参数时,inject() 照样走 injection "${key}" not found.。库的正确写法一定带默认值。
n8n 的 packages/frontend/@n8n/composables/src/useTelemetry.ts(源码):
const injected = hasInjectionContext() ? inject(TelemetryKey, null) : null
const instance = injected ?? getRegisteredTelemetry()
vueuse 的 packages/core/useSSRWidth/index.ts(源码):
export function useSSRWidth() {
// Avoid injection warning outside of components
const ssrWidth = hasInjectionContext() ? injectLocal(ssrWidthSymbol, null) : null
return typeof ssrWidth === 'number' ? ssrWidth : undefined
}
这里 null 与 undefined 是被刻意分开的:null 表示「不在注入上下文里」,undefined 表示「在上下文里但值不可用」。两者如果混用,调用方就分不清是环境不对还是配置缺失。
版本与兼容
- 3.3.0 加入,PR:vuejs/core#8111(posva,2023-04-18 提交,2023-04-20 合并,+39 −3)。
- 文档:Composition API: Dependency Injection,明确标注 Only supported in 3.3+。vue-router 内部的使用可以看 useApi.ts。
- 3.3 以下直接 import 拿不到这个导出。TanStack Query 那个 PR 里维护者提到:升级需要 vue-demi 同步更新,否则用户会遇到编译错误;排查手段是
pnpm why vue-demi或pnpm update。 - 要给 3.2 及以下兜底,只能靠特征检测:
import * as vue from 'vue'
const canInject = typeof (vue as any).hasInjectionContext === 'function'
? (vue as any).hasInjectionContext()
: !!(vue as any).getCurrentInstance?.()
后半段的等价性依赖内部实现,只作为降级近似,不要当正式 API 用。
生产环境不是 no-op
函数体里没有 __DEV__ 分支,打包之后照样返回布尔值。用它做「有上下文就走注入、没有就走全局单例」的分支时,dev 与 prod 走的是同一套判断——这和只在 dev 生效的 warn 完全是两回事。
副作用在于:如果两条路径返回值的语义不一样(比如注入拿到的是组件级实例,全局兜底拿到的是单例),那么执行顺序、SSR 与客户端 hydration、微前端里多个 Vue 副本,都可能让分支翻转,同一段代码在 dev 里拿到 A、在构建产物里拿到 B。这类问题不会伴随任何警告出现,排查成本比一条 dev warn 高得多。
自查判据
inject()只在 setup 的同步段调用;需要跨await,先把值取出来存进局部变量,或者通过参数传递。- 库的守卫写成
hasInjectionContext() ? inject(Key, fallback) : fallback,第二个参数不能省。 - 要在非组件代码里读注入值,用
app.runWithContext(() => ...),但不要指望它里面的provide()生效。 - 需要 provide 的地方只有两个:组件 setup 的同步段、
app.provide()。 - 引入
hasInjectionContext()之前,先确认peerDependencies里 vue 下限 ≥ 3.3,并检查 vue-demi 版本。 - 同时依赖「注入值」和「全局兜底」时,确认两条路径返回的对象语义一致,否则先统一语义再谈分支。