第 19 章

setup() 执行上下文:调用时机、getCurrentInstance 与生命周期注册

第19章:setup() 执行上下文——调用时机、getCurrentInstance 与生命周期注册

onMounted(fn) 内部只有一行实质性代码:currentInstance.lifecycle.mounted.push(fn)——生命周期钩子不是"注册到 Vue 框架"的,而是被放进当前组件实例的一个数组里;这个数组在合适的时机被渲染器逐一调用。如果 currentInstance 是 null,你的钩子永远不会被调用,且不会报任何错误。

本章核心问题setup() 里发生的一切为什么在 await 之后就不能再用生命周期钩子?getCurrentInstance() 到底是什么,它指向哪里?

读完本章你将理解

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() 期间创建的所有 watchwatchEffectcomputed 都通过 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

本章小结

  1. setup() 是 Vue 3 组件挂载的神经中枢:它在 createComponentInstance 之后、setupRenderEffect 之前同步执行;在 setup() 开始时,全局变量 currentInstance 被设置为当前组件实例,在 setup() 返回后立即清除。这个窗口期是生命周期钩子注册、provide/inject 调用的唯一有效时机。

  2. currentInstance 是生命周期钩子的隐式参数onMounted(fn) 等所有生命周期钩子 API 内部都依赖 currentInstance 来知道"把这个钩子注册给哪个组件实例";currentInstance 为 null 时,钩子注册会静默失败,这是异步 setup 中最常见的隐性 bug。

  3. 异步 setup() 的 await 陷阱是设计决策,不是 bugsetupStatefulComponent 在调用 setup() 后立即同步清除 currentInstance;如果 setup 返回 Promise,Promise 内部的回调在 currentInstance 清除后才执行。要在异步操作后安全使用响应式数据,应在同步阶段创建 ref/reactive,然后在 async 代码中修改其 .value

  4. provide/inject 通过原型链实现层级查找:每个组件的 provides 对象以父组件的 provides 为原型,形成原型链;injectparent.provides 开始沿原型链向上查找,直到 appContext.provides;组件不能 inject 自己 provide 的值。

  5. EffectScope 是响应式副作用的生命周期管理器:setup() 期间创建的所有 watch/watchEffect/computed 都被注册到组件的 EffectScope 中;组件卸载时 scope.stop() 一次性停止所有副作用;在 setup 之外(事件处理器、全局函数)创建的 effect 不在这个 scope 中,需要手动管理停止逻辑。

本章评分
4.6  / 5  (11 评分)

💬 留言讨论