setup() 执行上下文:调用时机、getCurrentInstance 与生命周期注册
第19章:setup() 执行上下文——调用时机、getCurrentInstance 与生命周期注册
onMounted(fn)内部只有一行实质性代码:currentInstance.lifecycle.mounted.push(fn)——生命周期钩子不是"注册到 Vue 框架"的,而是被放进当前组件实例的一个数组里;这个数组在合适的时机被渲染器逐一调用。如果currentInstance是 null,你的钩子永远不会被调用,且不会报任何错误。
本章核心问题:setup() 里发生的一切为什么在 await 之后就不能再用生命周期钩子?getCurrentInstance() 到底是什么,它指向哪里?
读完本章你将理解:
setup()在组件挂载链路中的精确调用时机currentInstance全局变量的生命周期与作用- 为什么生命周期钩子必须在
setup()的同步阶段注册 - 异步
setup()的await陷阱及其根本原因 provide/inject与currentInstance的内部关系ComponentInternalInstance对象的关键字段
Level 1 · 你需要知道的(1-3年经验)
setup() 的执行时机:精确的时间线
组件挂载时间线:
createApp(App).mount('#app')
│
├── [同步] createComponentInstance()
│ 创建 instance 对象
│
├── [同步] 设置 currentInstance = instance ← setup() 开始前
│
├── [同步] 调用 setup(props, ctx)
│ ← 你的 setup() 代码在这里运行
│ ← 所有生命周期钩子必须在这里注册
│
├── [同步] 清除 currentInstance = null ← setup() 返回后
│
├── [同步] 处理 setup() 的返回值
│
└── [同步] setupRenderEffect()
├── 首次 render()
├── 挂载 DOM
└── 调用 onMounted 钩子(在 nextTick 后)
注意:在 Vue 2 的术语里:
beforeCreate:约等于 createComponentInstance 完成后
created:约等于 setup() 完成后
Options API 的 beforeCreate/created 也在这个阶段被调用
(但 setup() 更早于 beforeCreate,因为 setup 在 initOptions 之前)
currentInstance 是什么
currentInstance 是 @vue/runtime-core 包里的一个模块级全局变量:
// packages/runtime-core/src/component.ts
export let currentInstance: ComponentInternalInstance | null = null
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
currentInstance = instance
instance.scope.on()
}
export const unsetCurrentInstance = () => {
currentInstance && currentInstance.scope.off()
currentInstance = null
}
它的工作方式:
时间 ──────────────────────────────────────────────────────────────►
A 组件 setup() 执行期间
┌──────────────────────────────────────┐
│ currentInstance = instanceA │
│ │
│ onMounted(fn) │
│ ↓ 内部: currentInstance.mounted.push(fn) │
│ │
│ provide('key', value) │
│ ↓ 内部: currentInstance.provides.key = value │
└──────────────────────────────────────┘
A 组件 setup() 返回后:
currentInstance = null
B 组件 setup() 执行期间
┌──────────────────────────────────────┐
│ currentInstance = instanceB │
│ ... │
└──────────────────────────────────────┘
为什么 await 之后不能注册生命周期钩子
// ❌ 这是最常见的陷阱
const MyComp = defineComponent({
async setup() {
// 此时 currentInstance = instance
onMounted(() => console.log('mounted 1')) // ✓ 能注册
const data = await fetch('/api/data').then(r => r.json())
// await 之后:
// 1. 当前 JS 任务结束
// 2. setup() 的调用者(setupStatefulComponent)已经执行到了
// unsetCurrentInstance(),把 currentInstance 设为 null
// 3. 后续代码在另一个微任务中恢复执行
// 4. 此时 currentInstance = null
onMounted(() => console.log('mounted 2')) // ❌ currentInstance 是 null,这行代码等于 noop
// 没有报错!但 'mounted 2' 永远不会被打印
}
})
根本原因:
// packages/runtime-core/src/component.ts(简化)
function setupStatefulComponent(instance) {
const { setup } = instance.type
setCurrentInstance(instance) // ← 设置 currentInstance
const setupResult = callWithErrorHandling(setup, instance, ...)
unsetCurrentInstance() // ← 立即清除 currentInstance
// 注意:unsetCurrentInstance 是同步调用的
// 如果 setup() 返回了 Promise(async setup),
// unsetCurrentInstance 在 Promise 被创建后立即调用
// 而不是等 Promise resolve 后
if (isPromise(setupResult)) {
// setupResult 是 Promise
// 但 currentInstance 已经被清除了!
setupResult.then(result => finishSetup(instance, result))
// 在 then 里注册生命周期钩子,currentInstance 已经是 null
} else {
finishSetup(instance, setupResult)
}
}
getCurrentInstance() 的正确用法
import { getCurrentInstance } from 'vue'
// ✓ 正确:在 setup() 同步阶段调用
const MyComp = defineComponent({
setup() {
const instance = getCurrentInstance()
console.log(instance?.uid) // 当前组件的唯一 ID
// 常见用途:在自定义 composable 中获取组件上下文
// 用于库代码,不建议在业务代码中直接使用
return {}
}
})
// ❌ 错误:在 setup() 之外或 await 之后调用
const instance = getCurrentInstance() // null
// 在模板事件处理器中调用:getCurrentInstance() // null
// ❌ 错误:在 onMounted 中获取并存储,然后在异步回调中使用
setup() {
let storedInstance
onMounted(() => {
storedInstance = getCurrentInstance() // 在 onMounted 中可以获取!
setTimeout(() => {
// storedInstance 不是 null,但它已经是旧引用
// 如果组件被卸载,storedInstance 仍然存在,可能造成内存泄漏
}, 1000)
})
}
Level 2 · 它是怎么运行的(3-5年经验)
ComponentInternalInstance 的关键字段
// packages/runtime-core/src/component.ts(关键字段)
export interface ComponentInternalInstance {
// 标识字段
uid: number // 组件的唯一 ID(全局递增)
type: ConcreteComponent // 组件定义对象(或函数式组件函数)
parent: ComponentInternalInstance | null // 父组件实例
root: ComponentInternalInstance // 根组件实例
appContext: AppContext // 应用级别的上下文(全局组件、指令、provide)
// 数据字段
props: ShallowReactiveObject // 响应式的 props(shallowReactive 包裹)
attrs: Data // 未被 defineProps 声明的 attrs
slots: InternalSlots // 插槽
refs: Data // 模板 ref 收集
// 执行上下文
setupContext: SetupContext | null // { attrs, slots, emit, expose }
data: Data // Options API 的 data()
setupState: Data // setup() 返回对象(经 proxyRefs 处理)
// 渲染相关
subTree: VNode // 当前渲染的 subTree
effect: ReactiveEffect // 渲染副作用
update: SchedulerJob // 更新函数
render: InternalRenderFunction | null // render 函数
// 生命周期钩子(数组)
bc: LifecycleHook // beforeCreate
c: LifecycleHook // created
bm: LifecycleHook // beforeMount
m: LifecycleHook // mounted
bu: LifecycleHook // beforeUpdate
u: LifecycleHook // updated
bum: LifecycleHook // beforeUnmount
um: LifecycleHook // unmounted
a: LifecycleHook // activated(KeepAlive)
da: LifecycleHook // deactivated(KeepAlive)
ec: LifecycleHook // errorCaptured
// 状态标志
isMounted: boolean // 是否已挂载
isUnmounted: boolean // 是否已卸载
isDeactivated: boolean // 是否已停用(KeepAlive)
// provide/inject
provides: Data // 此组件提供的数据(原型链向父组件继承)
// 作用域
scope: EffectScope // 响应式作用域(管理所有 effect)
}
生命周期钩子的注册机制
所有 onXxx() 函数都是同一个模式:
// packages/runtime-core/src/apiLifecycle.ts
export const createHook = <T extends Function = () => any>(
lifecycle: LifecycleHooks
) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// 如果 target 为 null(在 setup 外部调用),静默忽略
!isInSSRComponentSetup && injectHook(lifecycle, hook, target)
// 对外暴露的 API
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
// ...
function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
if (target) {
// 将钩子函数推入实例对应的数组
const hooks = target[type] || (target[type] = [])
// 包装钩子:执行时恢复 currentInstance(让钩子内部可以调用 getCurrentInstance)
const wrappedHook =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
if (target.isUnmounted) return // 组件已卸载,不执行
pauseTracking() // 暂停响应式追踪
setCurrentInstance(target) // ← 恢复 currentInstance!
const res = callWithAsyncErrorHandling(hook, target, type, args)
unsetCurrentInstance()
resetTracking()
return res
})
if (prepend) {
hooks.unshift(wrappedHook)
} else {
hooks.push(wrappedHook)
}
return wrappedHook
}
}
关键洞察:生命周期钩子在执行时,会通过 setCurrentInstance(target) 再次恢复 currentInstance。这意味着:在 onMounted 的回调内部,getCurrentInstance() 是有效的!
setup() {
onMounted(() => {
// 此处可以调用 getCurrentInstance(),因为钩子执行时恢复了 currentInstance
const instance = getCurrentInstance()
console.log(instance?.uid) // ✓ 有效
})
}
setup() 的返回值处理
// packages/runtime-core/src/component.ts(简化)
function finishSetup(instance, setupResult) {
if (isFunction(setupResult)) {
// 返回函数 → 作为 render 函数
instance.render = setupResult
} else if (isObject(setupResult)) {
// 返回对象 → 作为 setup 状态(可在模板和 Options API 中访问)
// proxyRefs 会自动解包 ref(访问 .count 而不是 .count.value)
instance.setupState = proxyRefs(setupResult)
}
// 之后处理 Options API(data/computed/methods/watch...)
applyOptions(instance)
}
// 返回对象(最常见)
setup() {
const count = ref(0)
return { count }
// 模板中可以直接用 {{ count }},不需要写 {{ count.value }}
// 因为 proxyRefs 自动解包了 ref
}
// 返回函数(手写 render 函数)
setup() {
const count = ref(0)
return () => h('div', count.value) // 直接返回 render 函数
}
// 两者不能混用:如果返回函数,模板会被忽略
provide/inject 与 currentInstance 的关系
// packages/runtime-core/src/apiInject.ts
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
// 在 setup 外调用 provide:警告
console.warn(`provide() can only be used inside setup().`)
return
}
let provides = currentInstance.provides
// 如果当前组件的 provides 和父组件的相同
// 说明还没有自己的 provides 对象,需要创建
const parentProvides = currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
// 用父组件的 provides 作为原型,创建自己的 provides
provides = currentInstance.provides = Object.create(parentProvides)
}
provides[key as string] = value
}
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false,
): T | undefined {
const instance = currentInstance || currentRenderingInstance
if (instance) {
// 沿原型链向上查找:从父组件的 provides 开始找
const provides = instance.parent == null
? instance.appContext.provides // 根组件直接找 appContext
: instance.parent.provides // 其他组件从父组件开始找
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.proxy)
: defaultValue
}
}
}
provides 的原型链设计:
应用级 provides(appContext.provides):
{ globalKey: 'globalValue' }
↑(原型)
根组件的 provides:
{ rootKey: 'rootValue' }
↑(原型)
父组件 A 的 provides:
{ parentKey: 'parentValue' }
↑(原型)
子组件 B 的 provides:
{ childKey: 'childValue' }
inject('parentKey'):
从 B 的父组件(A)的 provides 开始查找
→ A.provides.parentKey → 找到 'parentValue' ✓
inject('rootKey'):
A.provides.rootKey → undefined(A 自己没有)
→ 沿原型链到根组件 → rootKey → 找到 'rootValue' ✓
inject('globalKey'):
→ 沿原型链到 appContext.provides → globalKey → 找到 ✓
setup() 执行阶段的完整上下文管理
setup() 执行期间的全局状态变化:
1. setCurrentInstance(instance)
→ currentInstance = instance
→ instance.scope.on() (响应式作用域激活)
2. setup(props, ctx) 执行
│
├── onMounted(fn)
│ → currentInstance.m.push(wrappedFn)
│
├── ref(0)
│ → 创建 ref,但此时不追踪(setup 阶段不需要追踪)
│
├── provide('key', value)
│ → currentInstance.provides.key = value
│
└── inject('parentKey')
→ currentInstance.parent.provides.parentKey
3. unsetCurrentInstance()
→ currentInstance = null
→ instance.scope.off() (响应式作用域停止活跃)
4. 处理返回值(finishSetup)
5. setupRenderEffect()
→ 创建 ReactiveEffect
→ 首次执行 render()(此时 currentRenderingInstance 被设置)
Level 3 · 设计文档与源码(资深开发者)
currentRenderingInstance vs currentInstance 的区别
// packages/runtime-core/src/componentRenderContext.ts
export let currentRenderingInstance: ComponentInternalInstance | null = null
// 在 render() 函数执行期间设置
export function setCurrentRenderingInstance(instance: ComponentInternalInstance | null) {
const prev = currentRenderingInstance
currentRenderingInstance = instance
if (instance) {
currentScopeId = instance.type.__scopeId || null
}
return prev
}
两者的区别:
currentInstance |
currentRenderingInstance |
|
|---|---|---|
| 设置时机 | setup() 执行期间 | render() 函数执行期间 |
| 用途 | 生命周期钩子注册、provide/inject | inject(在 render 函数内)、scoped CSS |
| 清除时机 | setup() 返回后 | render() 返回后 |
inject 同时检查两者,所以在 render 函数内(或模板中)也可以使用 inject:
// inject 的 instance 来源:
const instance = currentInstance || currentRenderingInstance
组件代理(ComponentPublicProxy)的工作原理
setup() 返回的对象会被渲染器通过一个 Proxy 代理,使得在模板中访问变量时能自动查找正确的来源:
// packages/runtime-core/src/componentPublicInstance.ts(简化)
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance
// 优先级顺序:
// 1. setup 返回值
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
return setupState[key] // 自动 unref(proxyRefs 的作用)
}
// 2. data
if (data !== EMPTY_OBJ && hasOwn(data, key)) {
return data[key]
}
// 3. props
if (props && hasOwn(normalizedProps, key)) {
return props[key]
}
// 4. 全局属性($el, $route, $store 等)
const publicGetter = publicPropertiesMap[key]
if (publicGetter) {
return publicGetter(instance)
}
// 5. 全局注册的属性(通过 app.config.globalProperties)
if (appContext.config.globalProperties[key]) {
return appContext.config.globalProperties[key]
}
}
}
EffectScope 与 setup() 阶段的响应式管理
// 每个组件实例有一个独立的 EffectScope
// setup() 期间 scope.on() 激活,所有在 setup 中创建的 effect 都被注册到这个 scope
// packages/reactivity/src/effectScope.ts
export class EffectScope {
active = true
effects: ReactiveEffect[] = []
cleanups: (() => void)[] = []
parent: EffectScope | undefined
run<T>(fn: () => T): T | undefined {
if (this.active) {
const currentEffectScope = activeEffectScope
try {
activeEffectScope = this
return fn()
} finally {
activeEffectScope = currentEffectScope
}
}
}
stop() {
if (this.active) {
let i, l
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()
}
for (i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]()
}
this.active = false
}
}
}
setup() 期间创建的所有 watch、watchEffect、computed 都通过 activeEffectScope 注册到组件的 scope 中。组件卸载时,scope.stop() 一次性停止所有这些副作用,防止内存泄漏。
expose() 的实现:限制外部访问
// packages/runtime-core/src/componentOptions.ts
// expose() 允许组件选择性地暴露接口给父组件(通过模板 ref 访问)
const setupContext: SetupContext = (instance.setupContext = {
attrs,
slots,
emit,
expose: (exposed?: Record<string, any>) => {
instance.exposed = exposed || {}
// 如果调用了 expose({}),外部通过 ref 访问到的对象就只有 exposed 里的内容
// 如果没有调用 expose,外部可以访问 setupState 的所有内容
}
})
// 子组件
const MyInput = defineComponent({
setup(props, { expose }) {
const inputRef = ref(null)
const inputValue = ref('')
// 只暴露 focus 方法给父组件
expose({
focus() {
inputRef.value?.focus()
}
})
// inputValue 不会被暴露,父组件无法直接访问
return { inputRef, inputValue }
}
})
// 父组件
<MyInput ref="myInput" />
// this.$refs.myInput.focus() → 可以调用
// this.$refs.myInput.inputValue → undefined(未暴露)
Level 4 · 边界与陷阱(全体适用)
陷阱1:在 Composable 函数中忘记传递 currentInstance
// 错误写法:Composable 在异步环境中被调用时,内部的生命周期钩子注册失败
// useFetch.js
export function useFetch(url) {
const data = ref(null)
onMounted(async () => { // 在 composable 内部注册钩子
data.value = await fetch(url).then(r => r.json())
})
return { data }
}
// 组件中(正确使用方式)
setup() {
const { data } = useFetch('/api') // ✓ 同步调用,currentInstance 有效
return { data }
}
// 错误使用方式
setup() {
async function init() {
const { data } = useFetch('/api') // ❌ 在异步函数内调用
// 如果 init 在 await 之后被调用,currentInstance 已经是 null
}
return {}
}
陷阱2:在 provide/inject 中使用响应式值
// 错误:provide 了一个 ref 的 .value(值的快照,不是响应式的)
const count = ref(0)
provide('count', count.value) // ❌ 提供的是数字 0,不是响应式 ref
// 正确:provide 整个 ref 对象
provide('count', count) // ✓ 提供响应式 ref
provide('count', readonly(count)) // ✓ 提供只读 ref(更安全)
// 或者使用 computed
provide('doubleCount', computed(() => count.value * 2))
// 子组件 inject
const count = inject('count') // 得到 ref 对象
// 使用时需要 .value
console.log(count.value)
// 或者用 toRef 解包
const { count: countVal } = toRefs({ count })
陷阱3:在同一个 setup 中注册了 provide,却在同一个组件中 inject
// 错误:组件不能 inject 自己 provide 的值
const MyComp = defineComponent({
setup() {
provide('myKey', 'myValue')
// ❌ 注入自己提供的值(inject 从父组件的 provides 开始查找)
const value = inject('myKey')
// value === undefined(inject 从 parent.provides 找,不是从自己的 provides 找)
return { value }
}
})
陷阱4:getCurrentInstance 的返回值在 onMounted 后可能失效
// getCurrentInstance 返回的是组件实例的引用
// 如果组件被卸载,实例上的某些属性会被清空
const instance = getCurrentInstance()
onMounted(() => {
setTimeout(() => {
if (instance.isUnmounted) {
// 组件已卸载,不要再访问 instance.props 等
return
}
// 此时访问 instance 可能引发问题
}, 5000)
})
// 更好的做法:使用 onUnmounted 标志位
let isActive = true
onUnmounted(() => { isActive = false })
onMounted(() => {
setTimeout(() => {
if (!isActive) return
// 安全地执行
}, 5000)
})
陷阱5:EffectScope 使 watch 的停止变得隐蔽
// watch 在 setup() 中创建时,被注册到组件的 EffectScope
// 组件卸载时,scope.stop() 会自动停止这些 watch
// 但如果你在 setup 之外(比如在事件处理器中)创建 watch,
// 它不会被自动停止!
setup() {
const stop = watchEffect(() => {
console.log(count.value)
})
// setup 中的 watchEffect 会在组件卸载时自动停止
// 不需要手动调用 stop()(但调用也无害)
// 问题:在事件处理器中创建的 watch
function onSomeEvent() {
watchEffect(() => { // ❌ 这个 watch 不在组件的 scope 中!
console.log(someRef.value)
})
// 即使组件卸载,这个 watch 仍然活跃,造成内存泄漏
}
// 解决:用 effectScope 或手动管理
function onSomeEvent() {
const stop = watchEffect(() => {
console.log(someRef.value)
})
onUnmounted(stop) // 组件卸载时手动停止
}
return {}
}
陷阱6:defineExpose 和 expose() 在不同使用方式下的行为差异
// 使用 <script setup>(最常见)
// defineExpose 宏:明确暴露接口
// 如果没有调用 defineExpose,外部通过 ref 访问到的是空对象 {}
// <script setup>
const count = ref(0)
defineExpose({ count })
// 父组件 ref.value.count 可以访问
// 如果没有 defineExpose:
// 父组件 ref.value 是 {}(空对象)
// 使用 setup() 函数(Options API 方式)
// 默认情况下,外部通过 ref 可以访问 setupState 的所有内容
// 调用 expose({...}) 后,只能访问 expose 的内容
// 这两种方式的默认行为相反!
// <script setup>: 默认不暴露任何东西
// setup() 函数: 默认暴露所有 setupState
本章小结
-
setup() 是 Vue 3 组件挂载的神经中枢:它在
createComponentInstance之后、setupRenderEffect之前同步执行;在setup()开始时,全局变量currentInstance被设置为当前组件实例,在setup()返回后立即清除。这个窗口期是生命周期钩子注册、provide/inject 调用的唯一有效时机。 -
currentInstance 是生命周期钩子的隐式参数:
onMounted(fn)等所有生命周期钩子 API 内部都依赖currentInstance来知道"把这个钩子注册给哪个组件实例";currentInstance为 null 时,钩子注册会静默失败,这是异步 setup 中最常见的隐性 bug。 -
异步 setup() 的 await 陷阱是设计决策,不是 bug:
setupStatefulComponent在调用 setup() 后立即同步清除currentInstance;如果 setup 返回 Promise,Promise 内部的回调在currentInstance清除后才执行。要在异步操作后安全使用响应式数据,应在同步阶段创建 ref/reactive,然后在 async 代码中修改其.value。 -
provide/inject 通过原型链实现层级查找:每个组件的
provides对象以父组件的provides为原型,形成原型链;inject从parent.provides开始沿原型链向上查找,直到appContext.provides;组件不能 inject 自己 provide 的值。 -
EffectScope 是响应式副作用的生命周期管理器:setup() 期间创建的所有
watch/watchEffect/computed都被注册到组件的EffectScope中;组件卸载时scope.stop()一次性停止所有副作用;在 setup 之外(事件处理器、全局函数)创建的 effect 不在这个 scope 中,需要手动管理停止逻辑。