07-Vue 生命周期实现原理

Vue 生命周期实现原理

深入剖析 Vue2 与 Vue3 生命周期钩子的注册机制、调用时机及底层源码实现,理解生命周期与组件渲染的深层关系。

一、前言

生命周期是 Vue 框架最核心的概念之一,它定义了组件从创建到销毁的完整过程。理解生命周期的实现原理,不仅能帮助我们在正确的时机执行逻辑,更能深入理解 Vue 的渲染机制、响应式系统与组件更新策略。

Vue2 与 Vue3 在生命周期设计上既有传承也有变革:Vue3 引入了 Composition API,setup 函数成为新的逻辑组织中心,生命周期钩子也随之发生了重要变化。本文将从源码层面剖析两者的实现差异,揭示生命周期背后的设计哲学。

二、核心内容

2.1 Vue2 生命周期钩子注册机制

Vue2 的生命周期钩子通过选项式 API 在组件定义时声明。在初始化阶段,Vue 会将这些钩子函数收集到实例的 $options 对象中。

// Vue2 生命周期选项定义
const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];

// 初始化时合并策略
function mergeHook(parentVal, childVal) {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal;
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook;
});

initLifecycle 阶段,Vue 会遍历 $options 中的生命周期配置,将其转换为内部调用队列。每个生命周期钩子实际上是一个函数数组,支持通过 mixin 多次注册同一钩子。

2.2 Vue3 生命周期钩子的变化

Vue3 对生命周期进行了重大调整,主要体现在两个方面:

命名变更

  • beforeDestroybeforeUnmount
  • destroyedunmounted

Composition API 引入

import { onMounted, onUpdated, onUnmounted } from 'vue'

export default {
  setup() {
    // 在 setup 中注册生命周期钩子
    onMounted(() => {
      console.log('组件已挂载')
    })

    onUpdated(() => {
      console.log('组件已更新')
    })

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

Vue3 的生命周期钩子需要在 setup 函数的同步执行期间调用,它们通过全局上下文关联到当前正在初始化的组件实例。

2.3 钩子调用时机源码分析

Vue2 生命周期调用流程

Vue2 的生命周期调用贯穿在 _init$mount_update 方法中:

// core/instance/init.js
Vue.prototype._init = function(options) {
  const vm = this;
  
  // 合并选项
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  );
  
  // 初始化生命周期
  initLifecycle(vm);
  initEvents(vm);
  initRender(vm);
  
  // 调用 beforeCreate
  callHook(vm, 'beforeCreate');
  
  // 初始化注入、响应式数据
  initInjections(vm);
  initState(vm);  // props、methods、data、computed、watch
  initProvide(vm);
  
  // 调用 created
  callHook(vm, 'created');
  
  // 执行挂载
  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};

// 通用的钩子调用函数
function callHook(vm, hook) {
  const handlers = vm.$options[hook];
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm);
      } catch (e) {
        handleError(e, vm, `${hook} hook`);
      }
    }
  }
}
Vue3 生命周期调用流程

Vue3 将生命周期调用分散在 setupComponentrender 流程中:

// runtime-core/component.ts
function setupComponent(instance) {
  const { props, children } = instance.vnode;
  const isStateful = isStatefulComponent(instance);
  
  initProps(instance, props, isStateful, isStateful);
  initSlots(instance, children);
  
  const setupResult = isStateful
    ? setupStatefulComponent(instance)
    : undefined;
  
  return setupResult;
}

function setupStatefulComponent(instance) {
  const Component = instance.type;
  
  // 创建 setup 上下文
  instance.accessCache = Object.create(null);
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
  
  const { setup } = Component;
  if (setup) {
    const setupContext = createSetupContext(instance);
    
    // 设置当前实例,供生命周期钩子注册使用
    setCurrentInstance(instance);
    
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [shallowReadonly(instance.props), setupContext]
    );
    
    unsetCurrentInstance();
    
    // 处理 setup 返回值
    handleSetupResult(instance, setupResult);
  }
}

2.4 生命周期与渲染的关系

生命周期与渲染流程紧密耦合。以 mounted 为例,它标志着虚拟 DOM 已经转换为真实 DOM 并插入文档:

// Vue2 挂载流程中的 mounted 调用
Vue.prototype._update = function(vnode, hydrating) {
  const vm = this;
  const prevEl = vm.$el;
  const prevVnode = vm._vnode;
  
  if (!prevVnode) {
    // 首次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
    
    // 新根节点,调用 mounted
    if (vm.$parent && vm.$parent._vnode) {
      vm.$parent._vnode.children = vm.$vnode;
    }
    callHook(vm, 'mounted');
  } else {
    // 更新渲染
    vm.$el = vm.__patch__(prevVnode, vnode);
    callHook(vm, 'updated');
  }
};

Vue3 中通过 queuePostRenderEffect 确保生命周期回调在渲染完成后执行:

// runtime-core/renderer.ts
const mountComponent = (initialVNode, container) => {
  const instance = initialVNode.component = createComponentInstance(initialVNode);
  
  setupComponent(instance);
  
  setupRenderEffect(instance, initialVNode, container);
};

const setupRenderEffect = (instance, initialVNode, container) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 首次渲染
      const subTree = (instance.subTree = renderComponentRoot(instance));
      patch(null, subTree, container);
      
      initialVNode.el = subTree.el;
      instance.isMounted = true;
      
      // 调用 mounted 钩子
      queuePostRenderEffect(() => {
        instance.a && invokeArrayFns(instance.a); // mounted hooks
      });
    } else {
      // 更新渲染
      const nextTree = renderComponentRoot(instance);
      const prevTree = instance.subTree;
      instance.subTree = nextTree;
      patch(prevTree, nextTree);
      
      // 调用 updated 钩子
      queuePostRenderEffect(() => {
        instance.u && invokeArrayFns(instance.u); // updated hooks
      });
    }
  };
  
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(updateComponent),
    instance.scope
  ));
  
  const updateComponent = (instance.update = () => effect.run());
  updateComponent();
};

2.5 setup 中的生命周期实现

Vue3 的 setup 中生命周期钩子通过全局状态管理当前实例:

// runtime-core/component.ts
let currentInstance = null;

export const getCurrentInstance = () => currentInstance;

export const setCurrentInstance = (instance) => {
  currentInstance = instance;
};

// onMounted 实现
export const onMounted = (hook, target = currentInstance) => {
  if (target) {
    // 将钩子推入 mounted 数组
    (target.m || (target.m = [])).push(hook);
  }
};

// onUnmounted 实现
export const onUnmounted = (hook, target = currentInstance) => {
  if (target) {
    (target.um || (target.um = [])).push(hook);
  }
};

这种设计允许在 setup 的任意嵌套函数中注册生命周期钩子,只要调用时存在当前实例上下文即可。

2.6 错误处理钩子

Vue 提供了 errorCaptured(Vue2)和 onErrorCaptured(Vue3)用于捕获组件树中的错误:

// Vue2 errorCaptured
export default {
  errorCaptured(err, vm, info) {
    console.error('捕获到错误:', err);
    console.error('出错的组件:', vm);
    console.error('错误信息:', info);
    
    // 返回 false 阻止错误继续向上传播
    return false;
  }
}
// Vue3 onErrorCaptured
import { onErrorCaptured } from 'vue'

export default {
  setup() {
    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err);
      // 返回 false 阻止传播
      return false;
    });
  }
}

错误处理钩子的调用遵循组件树向上冒泡的机制,直到被捕获或到达根组件。

三、Mermaid 图表

Vue2 生命周期完整流程

开始

初始化事件和生命周期

beforeCreate

初始化注入和响应式

created

有 el 选项?

等待 $mount 调用

编译模板

beforeMount

创建虚拟 DOM

Patch 到真实 DOM

mounted

数据变化?

beforeUpdate

重新渲染

updated

调用 $destroy?

beforeDestroy

拆卸观察者/子组件/事件监听

destroyed

结束

Vue2 vs Vue3 生命周期对比

Vue3 生命周期

Vue2 生命周期

更名

更名

beforeCreate

created

beforeMount

mounted

beforeUpdate

updated

beforeDestroy

destroyed

beforeCreate

created

beforeMount

mounted

beforeUpdate

updated

beforeUnmount

unmounted

四、代码示例

示例 1:Vue2 生命周期完整演示

// lifecycle-demo.vue
export default {
  name: 'LifecycleDemo',
  
  data() {
    return {
      message: 'Hello Vue2',
      timer: null
    };
  },
  
  beforeCreate() {
    console.log('beforeCreate: 实例初始化完成,数据观测和事件未设置');
    // 此时无法访问 data、computed、methods
    // console.log(this.message); // undefined
  },
  
  created() {
    console.log('created: 数据观测完成,可以访问数据');
    console.log(this.message); // 'Hello Vue2'
    
    // 适合进行异步数据获取
    this.fetchData();
  },
  
  beforeMount() {
    console.log('beforeMount: 模板编译完成,即将挂载到 DOM');
    console.log(this.$el); // undefined
  },
  
  mounted() {
    console.log('mounted: 已挂载到 DOM');
    console.log(this.$el); // DOM 元素
    
    // 启动定时器
    this.timer = setInterval(() => {
      this.message = new Date().toLocaleTimeString();
    }, 1000);
  },
  
  beforeUpdate() {
    console.log('beforeUpdate: 数据已变化,即将重新渲染');
    // 可以获取更新前的 DOM 状态
  },
  
  updated() {
    console.log('updated: 重新渲染完成');
    // 避免在此处修改数据,可能导致无限循环
  },
  
  beforeDestroy() {
    console.log('beforeDestroy: 实例即将销毁');
    // 清理工作
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  },
  
  destroyed() {
    console.log('destroyed: 实例已销毁');
  },
  
  methods: {
    fetchData() {
      // 模拟异步请求
      setTimeout(() => {
        this.message = '数据加载完成';
      }, 500);
    }
  }
};

示例 2:Vue3 Composition API 生命周期

<!-- lifecycle-composition.vue -->
<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} from 'vue';

export default {
  name: 'LifecycleComposition',
  
  setup() {
    const count = ref(0);
    let timer = null;
    
    // 等效于 beforeCreate + created
    console.log('setup: 在组件创建时执行');
    
    onBeforeMount(() => {
      console.log('onBeforeMount: 挂载前');
    });
    
    onMounted(() => {
      console.log('onMounted: 已挂载');
      
      // 启动定时器
      timer = setInterval(() => {
        console.log('定时器运行中...');
      }, 2000);
    });
    
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate: 更新前');
    });
    
    onUpdated(() => {
      console.log('onUpdated: 更新完成');
    });
    
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount: 卸载前');
      // 清理副作用
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    });
    
    onUnmounted(() => {
      console.log('onUnmounted: 已卸载');
    });
    
    onErrorCaptured((err, instance, info) => {
      console.error('捕获错误:', err.message);
      console.error('错误来源:', info);
      return false; // 阻止传播
    });
    
    const increment = () => {
      count.value++;
    };
    
    return {
      count,
      increment
    };
  }
};
</script>

示例 3:自定义生命周期组合式函数

// composables/useLifecycle.js
import { onMounted, onUnmounted } from 'vue';

/**
 * 自动清理的定时器
 */
export function useInterval(callback, delay) {
  let timer = null;
  
  onMounted(() => {
    if (delay !== null) {
      timer = setInterval(callback, delay);
    }
  });
  
  onUnmounted(() => {
    if (timer) {
      clearInterval(timer);
      timer = null;
    }
  });
  
  return {
    clear: () => {
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    }
  };
}

/**
 * 监听窗口大小变化
 */
export function useWindowResize(callback) {
  const handler = () => {
    callback({
      width: window.innerWidth,
      height: window.innerHeight
    });
  };
  
  onMounted(() => {
    window.addEventListener('resize', handler);
    handler(); // 立即执行一次
  });
  
  onUnmounted(() => {
    window.removeEventListener('resize', handler);
  });
}

// 使用示例
import { ref } from 'vue';
import { useInterval, useWindowResize } from './composables/useLifecycle.js';

export default {
  setup() {
    const count = ref(0);
    const windowSize = ref({ width: 0, height: 0 });
    
    // 使用组合式函数
    useInterval(() => {
      count.value++;
    }, 1000);
    
    useWindowResize((size) => {
      windowSize.value = size;
    });
    
    return {
      count,
      windowSize
    };
  }
};

五、常见问题

Q1:为什么 beforeCreate 中无法访问 data

因为在 beforeCreate 阶段,Vue 尚未执行 initState,数据观测(data observation)和事件系统还未初始化。此时实例只有基本的属性和事件配置,无法访问响应式数据、计算属性和方法。

Q2:createdmounted 的区别是什么?

  • created:实例已创建,数据观测完成,但 DOM 尚未生成,无法访问 $el
  • mounted:虚拟 DOM 已渲染为真实 DOM 并插入文档,可以访问 $el

如果需要在 DOM 操作后执行逻辑(如初始化第三方图表库),必须在 mounted 中进行。

Q3:Vue3 中 setup 替代了哪些生命周期?

setup 函数在 beforeCreatecreated 之间执行,本质上替代了这两个钩子。在 setup 中可以直接访问 propssetup 内部定义的响应式数据,但无法访问 datacomputed 等选项式 API 定义的内容。

Q4:为什么 updated 中修改数据会导致无限循环?

updated 钩子会在组件重新渲染后调用。如果在此钩子中修改响应式数据,会触发新的更新,再次进入 updated,形成无限循环。Vue 虽然有一些防护措施,但仍应避免这种写法。

Q5:onMounted 在异步 setup 中如何工作?

onMounted 必须在 setup 的同步执行阶段调用。如果 setup 返回 Promise(异步 setup),钩子注册需要在 await 之前完成:

setup() {
  onMounted(() => {
    console.log('这可以正常工作');
  });
  
  await fetchData(); // 异步操作
  
  // 以下代码在 await 之后,但 onMounted 已经注册成功
}

六、总结

生命周期是连接开发者与 Vue 内部机制的桥梁。通过本文的源码分析,我们可以得出以下关键结论:

  1. 注册机制:Vue2 通过选项合并将钩子收集到 $options,Vue3 通过全局上下文在 setup 中动态注册
  2. 调用时机:生命周期钩子嵌入在渲染流程的关键节点,与虚拟 DOM 的 patch 过程紧密耦合
  3. 设计演进:Vue3 的 Composition API 使生命周期使用更加灵活,支持更好的逻辑复用
  4. 清理义务:在 beforeDestroy/beforeUnmount 中清理副作用(定时器、事件监听、订阅)是最佳实践

理解生命周期的底层实现,有助于我们在复杂场景下做出正确的技术决策,写出更健壮的 Vue 应用。

七、思考题

  1. 在 Vue3 中,如果在一个嵌套的 setTimeout 回调中调用 onMounted,会发生什么?为什么?

  2. 设计一个自定义组合式函数 useLifecycleLogger,能够记录组件每个生命周期的执行时间并输出日志。

  3. 分析 Vue3 中 keep-alive 组件对生命周期的影响,为什么被缓存的组件会触发 activateddeactivated 而不是 mountedunmounted

  4. 在服务端渲染(SSR)场景下,哪些生命周期钩子不会执行?为什么 serverPrefetch 被引入?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李铁蛋zs

投喂博主,解锁更多实用前端技巧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值