Skip to content
⚠️ This article was written in 2020. Some content may be outdated.

Vue 3 ライフサイクルフック:Composition API版

Vue 3 Composition API 对生命周期钩子做了重新设计。过去我们在 Options API 中使用 mountedcreated 这些选项,现在在 setup() 函数中,需要改用对应的 onXxx 形式。这段时间在把内部组件库往 Composition API 迁移的过程中,踩了一些坑,整理一下。

Options API と Composition API フックの対応表

Options APIComposition API
beforeCreate(不需要,setup 本身就是)
created(不需要,setup 本身就是)
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured

beforeCreatecreated 在 Composition API 中不需要了,因为 setup() 的执行时机本身就在这两者之间。

基本的な使い方

typescript
import {
  ref,
  onMounted,
  onBeforeMount,
  onBeforeUnmount,
  onUnmounted,
  onBeforeUpdate,
  onUpdated,
  onErrorCaptured
} from 'vue'

interface User {
  id: number
  name: string
  avatar: string
}

export default {
  setup() {
    const users = ref<User[]>([])
    const loading = ref(true)
    const containerRef = ref<HTMLElement | null>(null)

    // 在 DOM 挂载之前,适合做初始化准备工作
    onBeforeMount(() => {
      console.log('组件即将挂载,此时还没有 DOM 节点')
    })

    // DOM 已经挂载完毕,可以安全操作 DOM
    onMounted(async () => {
      console.log('组件已挂载,DOM 可用')
      // 此时 containerRef.value 有值
      if (containerRef.value) {
        containerRef.value.style.opacity = '1'
      }

      try {
        const res = await fetch('/api/users')
        users.value = await res.json()
      } finally {
        loading.value = false
      }
    })

    // 响应式数据变更导致 DOM 重新渲染前
    onBeforeUpdate(() => {
      console.log('组件即将更新')
      // 可以在这里访问更新前的 DOM 状态
    })

    // DOM 已经更新完毕
    onUpdated(() => {
      console.log('组件已更新')
      // 不要在 updated 中修改响应式数据,可能导致无限循环
    })

    // 组件卸载之前,适合清理副作用
    onBeforeUnmount(() => {
      console.log('组件即将卸载')
    })

    // 组件已经卸载
    onUnmounted(() => {
      console.log('组件已卸载')
    })

    return {
      users,
      loading,
      containerRef
    }
  }
}

同じフックの複数登録

这是 Composition API 相比 Options API 的一个重要优势——同一个生命周期钩子可以注册多次,它们会按照注册顺序依次执行:

typescript
import { onMounted, onUnmounted } from 'vue'

// 每个 useXxx 都可以独立注册自己的生命周期逻辑
function useScrollTracking() {
  const scrollY = ref(0)

  function handleScroll() {
    scrollY.value = window.scrollY
  }

  onMounted(() => {
    window.addEventListener('scroll', handleScroll, { passive: true })
  })

  onUnmounted(() => {
    window.removeEventListener('scroll', handleScroll)
  })

  return { scrollY }
}

function useResizeObserver() {
  const width = ref(window.innerWidth)
  let observer: ResizeObserver | null = null

  onMounted(() => {
    observer = new ResizeObserver((entries) => {
      width.value = entries[0].contentRect.width
    })
    observer.observe(document.documentElement)
  })

  onUnmounted(() => {
    observer?.disconnect()
  })

  return { width }
}

export default {
  setup() {
    // 两个 use 函数各自注册了 onMounted 和 onUnmounted
    // 都会被正确执行,不会互相覆盖
    const { scrollY } = useScrollTracking()
    const { width } = useResizeObserver()

    return { scrollY, width }
  }
}

onErrorCaptured エラーバウンダリ

这个钩子在 Vue 3 中依然保留,可以用来构建类似 React ErrorBoundary 的错误捕获机制:

typescript
import { onErrorCaptured, ref, defineComponent, h } from 'vue'

const ErrorBoundary = defineComponent({
  name: 'ErrorBoundary',
  setup(_, { slots }) {
    const hasError = ref(false)
    const error = ref<Error | null>(null)

    onErrorCaptured((err: Error, instance, info) => {
      hasError.value = true
      error.value = err

      // 上报到监控平台
      console.error('组件错误被捕获:', {
        message: err.message,
        stack: err.stack,
        component: instance?.$options.name || 'Anonymous',
        lifecycleHook: info
      })

      // 返回 false 阻止错误继续向上传播
      return false
    })

    return () => {
      if (hasError.value && error.value) {
        return h('div', { class: 'error-boundary' }, [
          h('h3', '出了点问题'),
          h('p', error.value.message),
          h('button', {
            onClick: () => {
              hasError.value = false
              error.value = null
            }
          }, '重试')
        ])
      }
      return slots.default?.()
    }
  }
})

watchとの実行順序

一个容易忽略的点:在 setup() 中注册的 watch(非 watchEffect)默认是在组件更新之前执行的,而 onMounted 中发起的副作用也会正确触发 watch。但要注意 watch 的 flush 选项:

typescript
import { ref, watch, onUpdated } from 'vue'

export default {
  setup() {
    const count = ref(0)

    // 默认 pre:在组件更新前执行
    watch(count, () => {
      console.log('watch pre (默认)')
    })

    // post:在组件更新后执行,类似 onUpdated 但只在特定数据变化时触发
    watch(count, () => {
      console.log('watch post')
    }, { flush: 'post' })

    // sync:同步执行,很少用
    watch(count, () => {
      console.log('watch sync')
    }, { flush: 'sync' })

    onUpdated(() => {
      console.log('onUpdated')
    })

    // 点击后输出顺序:
    // watch pre -> watch sync (取决于注册顺序,sync 会先执行) -> onUpdated -> watch post
    // 实际上 sync 会在数据变更时立即执行,不受顺序影响
    return { count }
  }
}

实际输出顺序比较复杂,但核心原则是:sync 最先(数据变化时立即执行),然后是 pre(DOM 更新前),然后是 onUpdated(DOM 更新后),最后是 post 的 watch。

まとめ

  • Composition API 的生命周期钩子以 onXxx 函数形式在 setup() 中调用
  • beforeCreatecreated 不再需要,setup() 本身就覆盖了这个阶段
  • 每个钩子可以注册多次,按注册顺序执行,非常适合逻辑复用
  • onErrorCaptured 可以实现类似 React ErrorBoundary 的错误边界
  • 注意 watchflush 选项和生命周期钩子的执行顺序关系
  • onUnmounted 中务必清理事件监听、定时器、Observer 等副作用

MIT Licensed