尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Vue 3 Composition API 生命周期钩子:从 onMounted 到 onUnmounted 的实战指南

Vue 3 Composition API 生命周期钩子:从 onMounted 到 onUnmounted 的实战指南 1. 从“选项式”到“组合式”为什么我们需要重新理解Vue3的生命周期如果你是从Vue 2迁移过来的开发者或者刚开始学习Vue 3面对setup语法糖和Composition API时最困惑的点之一可能就是“我熟悉的created、mounted这些生命周期钩子现在该怎么用” 这种感觉我特别理解当初我也花了不少时间才把新旧两套体系在脑子里理顺。Vue 3引入的Composition API和script setup语法糖不仅仅是API形式的变化更是一种思维模式的转变——从“选项式”的配置思维转向“组合式”的逻辑组织思维。生命周期作为组件行为的关键控制点其使用方式也随之发生了根本性的变化。简单来说在Vue 3的Composition API世界里生命周期钩子不再是export default对象中的一个选项如mounted() {}而是变成了一系列需要从vue包中导入并在setup()函数内部或script setup顶层调用的函数。例如onMounted、onUpdated、onUnmounted等。这种变化带来的核心好处是逻辑的关注点分离和更好的可复用性。在Vue 2的Options API中一个功能相关的数据、计算属性、方法和生命周期钩子可能分散在data、computed、methods、mounted等不同选项中。而在Composition API中你可以将与某个特定功能相关的所有逻辑包括响应式状态、计算属性、函数以及生命周期钩子组织在同一个函数或同一个代码区域里这使得代码更容易理解和维护。对于使用TypeScript的开发者来说这套新API带来了更好的类型推断和开发体验。所有生命周期钩子函数都有明确的类型定义配合script setup语法糖几乎可以实现“开箱即用”的TypeScript支持无需额外的类型声明。本文将带你彻底搞懂在Vue 3 TypeScript环境下如何使用script setup语法糖来管理组件的生命周期并对比传统Options API的写法让你不仅能“用起来”更能“懂得为什么这么用”。2.script setup语法糖为组合式API而生的简洁范式在深入生命周期之前我们必须先理解承载它们的舞台——script setup。这不是一个必须的功能但它是目前Vue 3单文件组件SFC的推荐写法因为它极大地简化了Composition API的使用。2.1 基本形态与自动暴露传统的setup()函数写法需要返回一个对象模板才能访问其属性script langts import { ref, defineComponent } from vue; export default defineComponent({ setup() { const count ref(0); const increment () { count.value }; // 必须显式返回 return { count, increment }; } }); /script而使用script setup语法糖顶层的绑定变量、函数声明、import引入的内容会自动暴露给模板无需returnscript setup langts import { ref } from vue; const count ref(0); const increment () { count.value }; // 无需returncount和increment在模板中直接可用 /script template button clickincrement{{ count }}/button /template这种写法更简洁逻辑更集中。对于TypeScript在script setup中声明的类型也会被自动推导提供了近乎完美的类型支持。2.2 定义Props、Emits与组件在script setup中我们使用编译器宏Compiler Macros来定义组件的props和emits这些宏会在编译时被处理掉不会出现在运行时。定义Props使用defineProps宏并通常配合TypeScript的接口或类型字面量来获得完整的类型检查。script setup langts interface Props { title: string; // 可选属性带有默认值 count?: number; // 复杂类型 items?: Array{ id: number; name: string }; } // 使用接口定义props的类型 const props definePropsProps(); // 如果需要定义默认值可以使用withDefaults编译器宏 // const props withDefaults(definePropsProps(), { // count: 0, // items: () [] // }); console.log(props.title); // 类型安全地访问 /script定义Emits使用defineEmits宏来定义组件可以触发的事件及其载荷类型。script setup langts // 定义事件名称和载荷类型 const emit defineEmits{ // 语法 (eventName: [payloadType]) update:title: [string]; delete-item: [id: number]; // 没有载荷的事件 confirm: []; }(); const handleClick () { emit(update:title, New Title); // 类型检查第二个参数必须是string emit(delete-item, 123); // 正确 // emit(delete-item, 123); // 错误类型不匹配 }; /script定义普通组件与异步组件在script setup中引入的组件会自动注册无需通过components选项注册。script setup langts // 引入的组件可以直接在模板中使用 import MyChildComponent from ./MyChildComponent.vue; import { defineAsyncComponent } from vue; // 异步组件 const AsyncComponent defineAsyncComponent(() import(./AsyncComponent.vue) ); /script template MyChildComponent / AsyncComponent / /template注意defineProps、defineEmits、withDefaults、defineExpose等都是编译器宏它们不需要导入直接在script setup中使用即可。它们会在编译阶段被转换为正确的运行时代码。3. Vue 3生命周期钩子全解析在Setup中的调用方式现在进入核心部分。Vue 3的生命周期钩子函数都被设计成以on开头的函数形式需要在setup阶段同步调用。这意味着它们必须直接放在script setup的顶层作用域或setup()函数体内不能放在异步函数或者条件判断块内除非你非常清楚自己在做什么并且包裹在onMounted等内部。下表是Vue 3生命周期钩子与Vue 2选项的对照以及它们在Composition API中的对应函数Vue 2 选项式 APIVue 3 组合式 API (setup中)触发时机与用途beforeCreate无直接对应在实例初始化之后数据观测 (data observer) 和事件/侦听器配置之前被调用。在setup中此时setup函数本身正在执行你可以直接编写初始化代码。created无直接对应在实例创建完成后被立即调用。此时已完成数据观测、属性和方法的运算、watch/event事件回调。在setup中setup函数执行完毕即相当于此阶段。beforeMountonBeforeMount在挂载开始之前被调用相关的render函数首次被调用。mountedonMounted实例被挂载后调用这时组件已经出现在DOM中。这是进行DOM操作、初始化第三方库如图表、地图或发送初始Ajax请求的常见位置。beforeUpdateonBeforeUpdate数据变更导致虚拟DOM重新渲染和打补丁之前调用。可以在此访问更新前的DOM状态。updatedonUpdated由于数据变更导致的虚拟DOM重新渲染和打补丁之后调用。注意组件的任意更新都会触发若在此修改状态可能导致无限更新循环。应谨慎使用通常用于依赖更新后DOM状态的第三方库集成。beforeUnmount(Vue 2:beforeDestroy)onBeforeUnmount在卸载组件实例之前调用。此时实例依然完全可用。这是清理定时器、取消事件监听、销毁第三方实例的理想位置。unmounted(Vue 2:destroyed)onUnmounted卸载组件实例后调用。调用后组件实例的所有指令都被解除绑定所有事件监听器被移除所有子实例也都被卸载。errorCapturedonErrorCaptured捕获了后代组件传递的错误时调用。可以返回false阻止错误继续向上传播。renderTracked(Dev-only)onRenderTracked开发模式独有。跟踪虚拟DOM重新渲染时调用。用于调试哪个依赖导致了组件的重新渲染。renderTriggered(Dev-only)onRenderTriggered开发模式独有。当虚拟DOM重新渲染被触发时调用。用于调试是哪个依赖导致组件被重新渲染。activated(keep-alive)onActivated被keep-alive缓存的组件激活时调用。deactivated(keep-alive)onDeactivated被keep-alive缓存的组件失活时调用。3.1 在script setup中使用生命周期钩子代码示例让我们看一个综合的例子将上述钩子应用到实际场景中script setup langts import { ref, onMounted, onBeforeMount, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured } from vue; import Chart from some-chart-library; // 假设的图表库 // 响应式状态 const count ref(0); const chartInstance refChart | null(null); const containerRef refHTMLElement | null(null); // 1. onBeforeMount - 挂载前 onBeforeMount(() { console.log(3.1 组件挂载前 (onBeforeMount)); console.log(DOM元素引用 containerRef 当前为:, containerRef.value); // 此时为 null因为DOM还未创建 }); // 2. onMounted - 挂载后 onMounted(() { console.log(3.2 组件已挂载 (onMounted)); console.log(DOM元素引用 containerRef 现在为:, containerRef.value); // 此时可以访问到真实的DOM元素 // 典型用例初始化依赖DOM的第三方库 if (containerRef.value) { chartInstance.value new Chart(containerRef.value, { // 图表配置 }); } // 典型用例发起初始数据请求 fetchInitialData(); // 典型用例添加全局事件监听器注意清理 window.addEventListener(resize, handleResize); }); // 3. onUpdated - 更新后 onUpdated(() { console.log(3.3 组件已更新 (onUpdated), 当前count:, count.value); // 注意这里可能会被频繁调用。更新图表等操作最好用watch来优化。 // 例如如果图表数据依赖count应该用watch(count, () {更新图表})而不是在这里。 }); // 4. onBeforeUnmount - 卸载前 onBeforeUnmount(() { console.log(3.4 组件即将卸载 (onBeforeUnmount)); // 关键清理工作必须在这里做 if (chartInstance.value) { chartInstance.value.destroy(); // 销毁图表实例防止内存泄漏 chartInstance.value null; } // 移除事件监听器 window.removeEventListener(resize, handleResize); }); // 5. onUnmounted - 卸载后 onUnmounted(() { console.log(3.5 组件已卸载 (onUnmounted)); // 此时所有子组件已卸载实例已完全清理。 }); // 6. onErrorCaptured - 错误捕获 onErrorCaptured((err, instance, info) { console.error(3.6 捕获到组件错误:, err, info); // 可以在此上报错误日志 // 返回 false 阻止错误继续向上传播 return false; }); // 工具函数 function fetchInitialData() { // 模拟数据请求 console.log(发起初始请求...); } function handleResize() { console.log(窗口大小改变); if (chartInstance.value) { chartInstance.value.resize(); } } function increment() { count.value; } /script template div h2生命周期演示/h2 div refcontainerRef stylewidth: 400px; height: 300px; border: 1px solid #ccc; !-- 图表容器 -- /div pCount: {{ count }}/p button clickincrement增加 Count/button /div /template3.2 生命周期钩子的执行顺序与实战要点通过上面的代码我们可以清晰地看到在script setup中生命周期钩子的执行顺序和时机。有几个关键点需要在实际开发中特别注意onMounted不保证所有子组件都已挂载onMounted钩子只保证当前组件本身的DOM已经挂载。如果需要在所有子组件都挂载后执行操作类似于Vue 2中在mounted里使用$nextTick你需要在onMounted内部使用await nextTick()。import { onMounted, nextTick } from vue; onMounted(async () { // 等待一个微任务周期确保子组件DOM也已更新 await nextTick(); console.log(现在可以安全地操作所有子组件的DOM了); });onUpdated的陷阱onUpdated会在每次组件因为任何响应式数据变化而更新后触发。如果你在onUpdated里修改了响应式数据很容易导致无限循环更新。绝大多数情况下你应该使用watch或watchEffect来响应数据变化而不是onUpdated。onUpdated更适合用于那些你确实需要在DOM更新后执行但又无法通过响应式数据依赖精确描述的场景例如集成一个非响应式的第三方库需要在其数据变化后手动同步。清理工作必须在onBeforeUnmount或onUnmounted中进行这是防止内存泄漏的黄金法则。任何在onMounted或setup中创建的副作用都需要清理包括定时器 (setInterval,setTimeout)事件监听器 (window.addEventListener,document.addEventListener)第三方库实例如图表、地图、编辑器EventBus订阅如果使用WebSocket连接 通常放在onBeforeUnmount中更安全因为此时组件实例还完全可用。组合式函数Composables中的生命周期你可以在自定义的组合式函数Composables内部使用生命周期钩子。这使得生命周期逻辑可以和业务逻辑一起被封装和复用。例如一个用于监听窗口大小的组合式函数// composables/useWindowSize.ts import { ref, onMounted, onBeforeUnmount } from vue; export function useWindowSize() { const width ref(window.innerWidth); const height ref(window.innerHeight); const update () { width.value window.innerWidth; height.value window.innerHeight; }; onMounted(() { window.addEventListener(resize, update); }); onBeforeUnmount(() { window.removeEventListener(resize, update); }); return { width, height }; }// 在组件中使用 script setup langts import { useWindowSize } from ./composables/useWindowSize; const { width, height } useWindowSize(); /script4. 与TypeScript的深度集成类型安全与高级模式Vue 3的Composition API天生对TypeScript友好script setup langts更是将这种友好度提升到了极致。生命周期钩子函数本身具有完善的类型定义但我们在使用中仍可以遵循一些模式来获得最佳的类型安全体验。4.1 为模板引用Ref标注精确类型在生命周期中操作DOM离不开模板引用ref。为其标注精确的类型可以避免null值错误并获得IDE的智能提示。script setup langts import { ref, onMounted } from vue; // 1. 为DOM元素引用标注类型 const inputRef refHTMLInputElement | null(null); const canvasRef refHTMLCanvasElement | null(null); const divRef refHTMLElement | null(null); // 通用元素 // 2. 为组件实例引用标注类型 (需要知道组件类型) import MyChildComponent from ./MyChildComponent.vue; const childCompRef refInstanceTypetypeof MyChildComponent | null(null); onMounted(() { // 现在访问属性是类型安全的 if (inputRef.value) { inputRef.value.focus(); // IDE会提示.focus()方法 console.log(inputRef.value.value); } if (childCompRef.value) { // 可以访问子组件通过defineExpose暴露的属性和方法 childCompRef.value.someExposedMethod(); } }); /script template input refinputRef typetext / canvas refcanvasRef/canvas MyChildComponent refchildCompRef / /template4.2 在异步生命周期中保持类型安全有时我们会在onMounted中执行异步操作如数据请求。正确处理异步流程和错误很重要。script setup langts import { ref, onMounted } from vue; interface UserData { id: number; name: string; email: string; } const userData refUserData | null(null); const isLoading ref(false); const error refError | null(null); onMounted(async () { isLoading.value true; error.value null; try { const response await fetch(/api/user); if (!response.ok) { throw new Error(HTTP error! status: ${response.status}); } const data: UserData await response.json(); // 类型断言为UserData userData.value data; } catch (err) { error.value err instanceof Error ? err : new Error(Unknown error); console.error(Failed to fetch user data:, error.value); } finally { isLoading.value false; } }); /script4.3 使用defineExpose暴露组件内部方法在script setup中组件默认是“封闭”的即父组件通过ref无法访问子组件内部的任何状态或方法。如果你需要向父组件暴露特定的API需要使用defineExpose编译器宏。子组件 Child.vue:script setup langts import { ref } from vue; const internalState ref(secret); const publicMethod () { console.log(公共方法被调用); internalState.value exposed; }; // 只有被暴露的内容父组件才能访问 defineExpose({ publicMethod, // internalState // 如果暴露父组件也能访问 }); /script父组件 Parent.vue:script setup langts import { ref, onMounted } from vue; import Child from ./Child.vue; const childRef refInstanceTypetypeof Child | null(null); onMounted(() { if (childRef.value) { childRef.value.publicMethod(); // 可以调用 // console.log(childRef.value.internalState); // 错误类型上不存在该属性除非子组件暴露了它 } }); /script template Child refchildRef / /template5. 常见陷阱、最佳实践与性能考量掌握了基本用法后我们来看看在实际项目中容易踩的坑以及如何优雅地使用生命周期。5.1 陷阱一在生命周期中修改响应式状态导致无限循环这是一个经典错误尤其在onUpdated中。// ❌ 错误示例在onUpdated中修改依赖数据导致无限循环 const count ref(0); onUpdated(() { if (count.value 10) { count.value; // 每次更新都会触发onUpdated从而又触发更新... } }); // ✅ 正确做法使用watch或业务逻辑控制 const count ref(0); watch(count, (newVal) { if (newVal 10) { // 在某种条件下修改count但要确保逻辑不会循环触发 // 通常更好的设计是让修改发生在用户交互或其他事件中 } }); // 或者使用其他生命周期如onMounted onMounted(() { const timer setInterval(() { if (count.value 10) { count.value; } else { clearInterval(timer); } }, 1000); });5.2 陷阱二忘记清理副作用这是内存泄漏的主要来源。务必成对出现addEventListener对应removeEventListenersetInterval对应clearInterval。script setup langts import { onMounted, onBeforeUnmount } from vue; onMounted(() { const timerId setInterval(() console.log(tick), 1000); const handler () console.log(click); document.addEventListener(click, handler); // ✅ 正确做法将清理函数保存在作用域内以便在卸载时调用 // 但更优雅的方式是使用独立的清理函数 }); // 更清晰的做法将清理逻辑集中到onBeforeUnmount中 let timerId: number; let clickHandler: () void; onMounted(() { timerId window.setInterval(() console.log(tick), 1000); clickHandler () console.log(click); document.addEventListener(click, clickHandler); }); onBeforeUnmount(() { if (timerId) clearInterval(timerId); if (clickHandler) document.removeEventListener(click, clickHandler); }); /script5.3 最佳实践使用watchEffect进行自动清理watchEffect会自动追踪其内部依赖并在副作用重新执行前清理上一次的副作用。这非常适合处理需要响应式依赖的清理逻辑。script setup langts import { watchEffect, ref } from vue; const searchQuery ref(); // watchEffect 会自动管理清理 const stopWatch watchEffect((onCleanup) { // 模拟一个搜索API调用 const timer setTimeout(() { console.log(搜索: ${searchQuery.value}); }, 500); // onCleanup 回调会在下次副作用执行前或监听器停止时被调用 onCleanup(() { console.log(清理上一次搜索: ${searchQuery.value}); clearTimeout(timer); }); }); // 如果需要手动停止监听器例如在组件卸载前 // onBeforeUnmount(() { // stopWatch(); // }); /script5.4 性能考量避免在onUpdated中执行昂贵操作由于onUpdated在每次组件更新后都会触发应避免在此钩子中进行复杂的计算或DOM操作。对于依赖数据变化的副作用优先使用watch或computed。// ❌ 不推荐在onUpdated中操作DOM const data ref(/* ... */); onUpdated(() { updateChart(data.value); // 每次更新都重绘图表性能差 }); // ✅ 推荐使用watch精确控制 watch(data, (newData) { updateChart(newData); // 只有data变化时才重绘 }, { deep: true }); // 如果data是对象可能需要深度监听5.5 组合式函数中的生命周期封装与复用将生命周期逻辑封装进组合式函数是Composition API的核心优势。这能让你的组件代码更简洁逻辑更清晰。// composables/useIntersectionObserver.ts import { ref, onMounted, onBeforeUnmount, type Ref } from vue; export function useIntersectionObserver( target: RefHTMLElement | null, options?: IntersectionObserverInit ) { const isIntersecting ref(false); let observer: IntersectionObserver | null null; const stopObserver () { if (observer) { observer.disconnect(); observer null; } }; onMounted(() { if (!target.value) return; observer new IntersectionObserver((entries) { entries.forEach(entry { isIntersecting.value entry.isIntersecting; }); }, options); observer.observe(target.value); }); onBeforeUnmount(() { stopObserver(); }); // 返回一个停止观察的方法提供更灵活的控制 return { isIntersecting, stop: stopObserver }; }// 在组件中使用 script setup langts import { ref } from vue; import { useIntersectionObserver } from ./composables/useIntersectionObserver; const targetEl refHTMLElement | null(null); const { isIntersecting } useIntersectionObserver(targetEl, { threshold: 0.5 }); // 当元素50%进入视口时isIntersecting会变为true /script template div reftargetEl 我被观察着{{ isIntersecting ? 进入视口 : 未进入视口 }} /div /template通过这种方式复杂的生命周期和第三方API如IntersectionObserver被完美地封装和复用组件代码变得非常干净和声明式。这正是Vue 3组合式API和script setup语法糖想要带来的开发体验。理解并熟练运用生命周期钩子是构建健壮、可维护Vue 3应用的基础。
返回列表