编程 hasInjectionContext() 只看 currentInstance 与 currentApp:四个失效边界与自查判据

2026-11-15 00:04:18

hasInjectionContext() 判的只是 currentInstance 与 currentApp:四个边界与一条自查清单

Vue 3.3 加了 hasInjectionContext(),库作者用它包住内部的 inject(),避免污染终端用户的控制台。但它的返回值并不等于「注入能成功」,也不等于「provide 能用」。下面从警告现象、源码判定条件、四个失效边界依次拆开,最后给一份可以直接照着查的判据。

两种 inject 警告不是一回事

开发环境下控制台里会出现两种来源完全不同的警告,经常被混为一谈:

  1. [Vue warn]: inject() can only be used inside setup() or functional components. —— 调用位置没有注入上下文。
  2. [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 下照旧 warn provide() 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 版本。
  • 同时依赖「注入值」和「全局兜底」时,确认两条路径返回的对象语义一致,否则先统一语义再谈分支。

推荐文章

程序员茄子在线接单