Quiet
  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我

bajiu

  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我
Quiet主题
  • Vue3

Vue3生命周期

bajiu
前端

2025-08-19 11:05:00

生命周期总览(四个阶段)

创建 -> 挂载 -> 更新 -> 卸载

常用的 7 个钩子(Composition API 名称):

  • setup()(初始化阶段的入口)
  • onBeforeMount -> onMounted
  • onBeforeUpdate -> onUpdated
  • onBeforeUnmount -> onUnmounted

辅助/特殊钩子:

  • 错误边界:onErrorCaptured
  • KeepAlive:onActivated / onDeactivated
  • 调试依赖跟踪(开发定位渲染原因):onRenderTracked / onRenderTriggered
  • SSR:onServerPrefetch
  • 作用域销毁:onScopeDispose(与 effectScope() / 组件外部的响应式作用域相关

调用时机

组件从创建到挂载:实例、setup、渲染副作用

  1. 创建组件实例:createComponentInstance(vnode, parent)
    • 位置:runtime-core/src/component.ts
    • 作用:为 VNode 创建 组件实例(ComponentInternalInstance),初始化一堆字段(type/props/slots/provides/appContext/...),并给每种生命周期钩子预留数组存放位:
instance = {
  // ...
  // 这些数组就是注册生命周期钩子的“桶”
  bm: null,  // beforeMount hooks
  m: null,   // mounted hooks
  bu: null,  // beforeUpdate hooks
  u: null,   // updated hooks
  bum: null, // beforeUnmount hooks
  um: null,  // unmounted hooks
  a: null,   // activated
  da: null,  // deactivated
  ec: null,  // errorCaptured
  rtg: null, // renderTracked
  rtc: null, // renderTriggered
  // ...
}
  1. 运行组件初始化:setupComponent(instance, isSSR)
  • 位置:component.ts
  • 做两件事:
    • 处理 props/slots
    • 状态化组件:setupStatefulComponent(instance)

setupStatefulComponent(instance)

  • 拉出 setup(props, ctx) 并执行(可能是 async):
setCurrentInstance(instance)
const setupResult = setup(shallowReadonly(instance.props), setupContext)
unsetCurrentInstance()
  • 注册钩子就在 setup() 内部完成,此时只是把回调塞进 instance.xxx 的数组里;不会触发调用。
  • 返回值处理:
    • 若 setup 返回 函数:视作 render 函数保存到 instance.render。
    • 若返回 对象:挂到 instance.setupState,渲染时通过 proxy 暴露给模板。
    • 若是 Promise:交给 Suspense 机制,挂载会延后。
  1. 建立渲染副作用:setupRenderEffect(instance, initialVNode, container, anchor, ... )
  • 位置:renderer.ts
  • 核心:创建一个 响应式副作用(ReactiveEffect),在其回调内 执行渲染/打补丁:
instance.update = effect(function componentEffect() {
  if (!instance.isMounted) {
    // 首次挂载 -> patch(subTree)
    callHook(instance.bm)  // onBeforeMount
    subTree = renderComponentRoot(instance)
    patch(null, subTree, container, anchor, instance)
    instance.isMounted = true
    queuePostRenderEffect(instance.m, parentSuspense) // onMounted (post-flush)
  } else {
    // 更新 -> 重新 render + patch
    callHook(instance.bu)  // onBeforeUpdate
    const nextTree = renderComponentRoot(instance)
    const prevTree = instance.subTree
    instance.subTree = nextTree
    patch(prevTree, nextTree, container, anchor, instance)
    queuePostRenderEffect(instance.u, parentSuspense) // onUpdated (post-flush)
  }
}, scheduler)
  • 挂载/更新钩子调用点:
    • onBeforeMount:首次 patch 前
    • onMounted:patch 完后,通过队列延后(post-flush)
    • onBeforeUpdate:更新 patch 前
    • onUpdated:更新 patch 后,同样延后(post-flush)

钩子注册进实例:injectHook

  • 位置:apiLifecycle.ts
  • 每个 onXXX API 本质是调用 injectHook(lifecycle, hook, targetInstance):
export function onMounted(hook, target = currentInstance) {
  injectHook('m', hook, target) // 'm' 对应 instance.m
}

function injectHook(type, hook, target) {
  const list = target[type] || (target[type] = [])
  // 经过 error boundary 包装,保证抛错被上层捕获
  const wrapped = (...args) => callWithAsyncErrorHandling(hook, target, type, args)
  list.push(wrapped)
}
  • currentInstance 由 setCurrentInstance/unsetCurrentInstance 在执行 setup() 时设置,保证在 setup() 里调用 onMounted(...) 能找到正确的组件实例。

父子先后顺序

  1. 首次挂载(先父后子,mounted 反过来)
  • 渲染时流程(深度优先):

    1. patch(parent) → processComponent → mountComponent
    2. setupRenderEffect(parent) 执行 render 得到 subTree
    3. patch(null, subTree):这里会先递归挂载子组件
    4. 等子都挂好后,父的 onMounted 通过 post 队列 统一调用

因为 子组件的 onMounted 也走 post 队列,但它们被 更早推入队列(子先完成自身 patch),所以 子 mounted 先于父 mounted。

  1. 更新(父包子)
  • 父更新时:
    1. 先 callHook(parent.bu)
    2. patch 过程中 触发子组件的更新 effect(所以子 bu/u 在父 u 之前)
    3. 最后队列里按入队顺序 先跑完子 updated,再跑父 updated
  1. 卸载(父宣布、子先执行)
  • unmountComponent(instance):
callHook(instance.bum) // beforeUnmount
// 递归卸载子树
unmount(subTree, instance) // 子先 unmount
queuePostRenderEffect(instance.um) // 最后父的 unmounted 入队
  • 因为对子树先执行,所以观察到的顺序是 父 beforeUnmount → 子 beforeUnmount/子 unmounted → 父 unmounted。

Options API & Composition API 对照

意图/时机 Options API(Vue 3 仍可用) Composition API
初始化(早期逻辑) beforeCreate / created * setup()
首次挂载前/后 beforeMount / mounted onBeforeMount / onMounted
更新前/后 beforeUpdate / updated onBeforeUpdate / onUpdated
卸载前/后 beforeDestroy - beforeUnmount / destroyed - unmounted onBeforeUnmount / onUnmounted
错误捕获 errorCaptured onErrorCaptured

KeepAlive / Suspense / SSR

  1. <KeepAlive>
  • 被缓存的组件 不会卸载,因此:
    • 不会再触发 onBeforeUnmount/onUnmounted
    • 切换可见性时触发 onDeactivated(离开)与 onActivated(再次显示)
  1. <Suspense> 与 async setup
  • 如果 setup 返回 Promise(或使用 async setup),渲染会等待其 resolve:
  1. SSR(服务端渲染)
  • 只能在服务端使用的钩子:onServerPrefetch(可与客户端 hydration 对齐数据)
  • 客户端首次激活时仍会按正常顺序触发生命周期,但需注意避免重复取数(可用同构缓存、useQuery 等状态层处理)。

watch / watchEffect 与生命周期的协作

  • 默认 watch/watchEffect 的回调是 渲染前(flush 'pre')执行。
  • 如果需要 DOM 更新后 再跑(比如读尺寸):watch(..., { flush: 'post' })。
  • 清理副作用:
watchEffect((onCleanup) => {
  const id = setInterval(doSomething, 1000)
  onCleanup(() => clearInterval(id)) // 组件卸载或依赖变化时自动清理
})

时序流程图

首次挂载

render → patch(Component)
 └─ processComponent → mountComponent
     ├─ createComponentInstance
     ├─ setupComponent
     │   └─ setupStatefulComponent
     │       └─ 执行 setup() → injectHook(注册 onXxx)
     └─ setupRenderEffect
         └─ effect():
             ├─ callHook(bm)             // onBeforeMount
             ├─ subTree = render()
             ├─ patch(null, subTree)     // 深度优先挂载子
             └─ queuePostRenderEffect(m) // onMounted 入队
[flushJobs 尾部] → 依次执行所有 mounted(先子后父)

更新

state change → queueJob(update)
[flushJobs]
 └─ update():
     ├─ callHook(bu)             // onBeforeUpdate
     ├─ nextTree = render()
     ├─ patch(prev, next)        // 递归更新子
     └─ queuePostRenderEffect(u) // onUpdated 入队
[尾部] 执行 updated(先子后父)

卸载

unmountComponent
 ├─ callHook(bum)                // onBeforeUnmount
 ├─ unmount(subTree)             // 子先卸
 └─ queuePostRenderEffect(um)    // onUnmounted 父最后
[尾部] 执行 unmounted

常见时序: 子 mounted 先于父、updated 先子后父、KeepAlive 不触发 unmounted、改触发 activated/deactivated。

源码阅读流程

  • renderer.ts:patch / processComponent / mountComponent / setupRenderEffect
  • component.ts:createComponentInstance / setupComponent / setupStatefulComponent
  • apiLifecycle.ts:onMounted/onUpdated/... -> injectHook
  • scheduler.ts:queueJob / queuePostFlushCb / flushJobs
  • components/KeepAlive.ts、components/Suspense.ts

手动实现一下生命周期

Mini-vue-lifecycle.js:

  • 生命周期注册 API:onBeforeMount/onMounted/onBeforeUpdate/onUpdated/onBeforeUnmount/onUnmounted
  • 简易调度器(queueJob/queuePostFlushCb/flushJobs)与 nextTick
  • 组件实例化、setup、render、setState 触发更新
  • Parent/Child 示例验证”子 mounted 先于父” “updated 先子后父” “unmounted 顺序”等
// mini-vue-lifecycle.js
// 实现生命周期:
// onBeforeMount/onMounted/onBeforeUpdate/onUpdated/onBeforeUnmount/onUnmounted
// + 简易调度器(postFlush 队列)+ nextTick + setState 触发更新。

/******************* 调度器(scheduler) *******************/
const jobQueue = new Set();
const postFlushCbs = new Set();
let isFlushing = false;
const p = Promise.resolve();

function queueJob(job) {
  jobQueue.add(job);
  if (!isFlushing) flushJobs();
}

function queuePostFlushCb(cb) {
  postFlushCbs.add(cb);
  if (!isFlushing) flushJobs();
}

function flushJobs() {
  isFlushing = true;
  p.then(() => {
    // 1) 执行渲染 job(可能在执行中新增 job,Set 去重)
    jobQueue.forEach(job => job());
    jobQueue.clear();
    // 2) 执行 postFlush 回调(保证 DOM 已稳定)
    postFlushCbs.forEach(cb => cb());
    postFlushCbs.clear();
  }).finally(() => {
    isFlushing = false;
  });
}

export function nextTick(fn) {
  return fn ? p.then(fn) : p;
}

/******************* 生命周期 API 与实例结构 *******************/
let currentInstance = null;

function setCurrentInstance(i) { currentInstance = i; }
function unsetCurrentInstance() { currentInstance = null; }

function createHookRegistrar(key) {
  return function register(hook) {
    const i = currentInstance;
    if (!i) throw new Error(`on${key} called without active instance`);
    (i[key] || (i[key] = [])).push(hook);
  };
}

export const onBeforeMount   = createHookRegistrar('bm');
export const onMounted       = createHookRegistrar('m');
export const onBeforeUpdate  = createHookRegistrar('bu');
export const onUpdated       = createHookRegistrar('u');
export const onBeforeUnmount = createHookRegistrar('bum');
export const onUnmounted     = createHookRegistrar('um');

/******************* 组件实例与渲染 *******************/
function createComponentInstance(vnode, parent) {
  return {
    vnode,
    type: vnode.type,
    parent,
    isMounted: false,
    // 容纳生命周期函数的“桶”
    bm: null, m: null, bu: null, u: null, bum: null, um: null,
    // 状态 & 渲染
    state: {},
    render: null,
    subTree: null,
    el: null,
    update: null,
  };
}

function mountComponent(initialVNode, container, parent) {
  const instance = (initialVNode.component = createComponentInstance(initialVNode, parent));
  const Component = initialVNode.type;

  // 执行 setup,允许在其中注册生命周期
  setCurrentInstance(instance);
  const setupResult = Component.setup ? Component.setup(Component.props || {}) : {};
  unsetCurrentInstance();

  // setup 返回对象作为 state;render 从组件取
  instance.state = setupResult || {};
  instance.render = Component.render; // render(ctx) => DOM Element

  // 建立一次“渲染副作用”:区分首次挂载/后续更新
  instance.update = function componentEffect() {
    if (!instance.isMounted) {
      // beforeMount
      callHooks(instance.bm);
      // 渲染
      const el = instance.render(createRenderContext(instance));
      instance.el = el;
      container.appendChild(el);
      instance.isMounted = true;
      // mounted 延后(postFlush)
      queuePostFlushCb(() => callHooks(instance.m));
    } else {
      // beforeUpdate
      callHooks(instance.bu);
      // 简化:重新渲染整棵节点(diff 省略)
      const newEl = instance.render(createRenderContext(instance));
      instance.el.replaceWith(newEl);
      instance.el = newEl;
      // updated 延后
      queuePostFlushCb(() => callHooks(instance.u));
    }
  };

  // 首次执行
  queueJob(instance.update);
}

function unmountComponent(vnode) {
  const instance = vnode.component;
  if (!instance) return;
  // beforeUnmount 先同步执行
  callHooks(instance.bum);
  // 移除 DOM(子树此处忽略递归以简洁)
  if (instance.el && instance.el.parentNode) {
    instance.el.parentNode.removeChild(instance.el);
  }
  // unmounted 延后
  queuePostFlushCb(() => callHooks(instance.um));
}

function callHooks(list) {
  if (Array.isArray(list)) list.forEach(fn => { try { fn(); } catch (e) { console.error(e); } });
}

function createRenderContext(instance) {
  // 暴露给 render 的上下文:state + setState
  return {
    ...instance.state,
    setState(patch) {
      const next = typeof patch === 'function' ? patch(instance.state) : { ...instance.state, ...patch };
      instance.state = next;
      queueJob(instance.update);
    },
    el: () => instance.el,
  };
}

/******************* 运行时(应用入口) *******************/
export function h(type, props) { return { type, props, component: null }; }

export function createApp(Root) {
  return {
    mount(selectorOrEl) {
      const container = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl;
      if (!container) throw new Error('Container not found');
      const vnode = h(Root, {});
      mountComponent(vnode, container, null);
      return {
        unmount() { unmountComponent(vnode); },
        _instance: vnode.component,
      };
    }
  };
}

/******************* 父子组件与顺序验证 *******************/
export const Child = {
  setup() {
    onBeforeMount(() => console.log('%c[child] beforeMount','color:#999'));
    onMounted(() => console.log('%c[child] mounted','color:#2c7'));
    onBeforeUpdate(() => console.log('%c[child] beforeUpdate','color:#999'));
    onUpdated(() => console.log('%c[child] updated','color:#2c7'));
    onBeforeUnmount(() => console.log('%c[child] beforeUnmount','color:#999'));
    onUnmounted(() => console.log('%c[child] unmounted','color:#2c7'));
    return { count: 0 };
  },
  render(ctx) {
    const el = document.createElement('div');
    el.className = 'child';
    el.textContent = `Child count = ${ctx.count}`;
    return el;
  }
};

export const Parent = {
  setup() {
    onBeforeMount(() => console.log('%c[parent] beforeMount','color:#999'));
    onMounted(() => console.log('%c[parent] mounted','color:#2c7'));
    onBeforeUpdate(() => console.log('%c[parent] beforeUpdate','color:#999'));
    onUpdated(() => console.log('%c[parent] updated','color:#2c7'));
    onBeforeUnmount(() => console.log('%c[parent] beforeUnmount','color:#999'));
    onUnmounted(() => console.log('%c[parent] unmounted','color:#2c7'));
    return { n: 0 };
  },
  render(ctx) {
    const wrap = document.createElement('div');
    wrap.className = 'parent';

    const title = document.createElement('div');
    title.textContent = `Parent n = ${ctx.n}`;
    wrap.appendChild(title);

    // 简易“子组件内嵌”演示:直接创建一个 Child 实例并挂在到 wrap
    const childVNode = h(Child, {});
    mountComponent(childVNode, wrap, null);

    // 控制按钮
    const btn = document.createElement('button');
    btn.textContent = 'Parent setState(+1)';
    btn.addEventListener('click', () => ctx.setState(s => ({ ...s, n: s.n + 1 })));
    wrap.appendChild(btn);

    return wrap;
  }
};

使用方法:

import { createApp, Parent, nextTick } from './mini-vue-lifecycle.js'
const app = createApp(Parent).mount('#app')
// 触发一次更新:
app._instance.update(); // 或通过点击按钮
// 稍后:
// app.unmount();

HTML中:

<div id="app"></div>
<script type="module">
  import { createApp, Parent } from '/path/mini-vue-lifecycle.js'
  const app = createApp(Parent).mount('#app')
  // 手动触发一次更新(也可点页面按钮):
  app._instance.update()
  // 卸载:
  // app.unmount()
</script>

打开控制台,能看到打印顺序:

  • 首次挂载:[child] beforeMount → ... mounted → [parent] mounted(子先父后)
  • 更新:beforeUpdate / updated(先子后父)
  • 卸载:parent beforeUnmount - 子 beforeUnmount/unmounted - parent unmounted
上一篇

WebAssembly 加载错误 Incorrect response MIME type (Expected 'application/wasm')

下一篇

pnpm搭建Monorpeo项目

©2025 By bajiu.