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

资讯详情

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

Vue 3 onMounted生命周期钩子:从Options API到Composition API的演进与实践

Vue 3 onMounted生命周期钩子:从Options API到Composition API的演进与实践 1. 从“mounted”到“onMounted”一次心智模型的升级如果你是从 Vue 2 时代过来的开发者第一次在 Vue 3 的 Composition API 里看到onMounted这个函数可能会有点懵。以前我们习惯在options里写一个mounted() {}钩子现在却要调用一个函数。这不仅仅是语法上的改变背后是整个 Vue 3 响应式系统和逻辑组织方式的一次深刻演进。简单来说onMounted是 Vue 3 Composition API 提供的一个生命周期钩子注册函数。它的核心作用和 Vue 2 的mounted选项一模一样在组件挂载到 DOM 之后执行一些需要访问 DOM 元素或初始化外部资源的操作。比如你想获取一个div元素的引用ref来操作它的样式或者你想在组件一出现时就发起一个网络请求获取数据这些操作都应该放在onMounted里。但为什么 Vue 3 要把它设计成一个函数而不是一个选项呢这就要说到 Composition API 的精髓了。在 Options API 里mounted是一个孤立的选项你的初始化逻辑、数据获取逻辑、事件监听逻辑可能分散在data、methods、mounted、created等不同地方。当组件逻辑复杂时追踪一个功能的完整生命周期就变得困难。Composition API 鼓励我们将相关的逻辑数据、计算属性、方法、生命周期组合Compose在一起形成一个独立的、可复用的“逻辑关注点”。onMounted作为一个函数可以灵活地被放置在任何一个逻辑组合函数内部与它相关的响应式数据、方法紧密相邻大大提升了代码的可读性和可维护性。举个例子假设我们有一个用户资料卡片组件需要在挂载时从 API 获取用户数据。在 Options API 里你可能会在data里定义user在methods里定义fetchUser然后在mounted里调用this.fetchUser()。而在 Composition API 配合script setup的写法下代码会高度内聚script setup import { ref, onMounted } from vue import { fetchUserById } from /api/user const props defineProps({ userId: Number }) // 逻辑关注点用户数据获取 const user ref(null) const isLoading ref(false) const error ref(null) const loadUserData async () { isLoading.value true error.value null try { user.value await fetchUserById(props.userId) } catch (err) { error.value err.message } finally { isLoading.value false } } // 生命周期钩子与相关逻辑放在一起 onMounted(() { console.log(用户卡片组件已挂载开始获取用户数据) loadUserData() }) /script可以看到所有与“获取用户数据”相关的变量和函数都组织在了一起onMounted就紧挨着调用loadUserData的地方。这种写法让功能的边界非常清晰。无论是刚接触 Vue 3 的新手还是想重构老项目的资深开发者理解onMounted从“选项”到“函数”的这一转变是写好 Composition API 代码的第一步。2. onMounted 的基本语法与核心行为了解了onMounted的设计哲学我们来看看它的具体用法。它的语法非常简单但一些细节行为需要特别注意。2.1 基础调用方式onMounted是一个从vue包中导出的函数。它接受一个回调函数作为唯一参数。这个回调函数会在组件挂载完成后被调用。import { onMounted } from vue // 在组件的 setup 函数或 script setup 中调用 onMounted(() { // 你的初始化逻辑 console.log(组件已挂载) // 这里可以安全地访问 DOM 元素 const titleEl document.querySelector(h1) if (titleEl) { console.log(标题元素, titleEl) } })在script setup语法糖中你可以直接在最顶层调用onMounted就像上面的例子一样。如果是在传统的setup()函数写法里调用方式也完全相同。2.2 执行时机与“挂载后”的含义“挂载后”这个说法需要精确理解。它指的是组件的模板已经被编译并渲染成真实的 DOM 节点。这些 DOM 节点已经被插入到父容器通常是#app或某个具体的父元素中。此时子组件如果有的话也已经被挂载。这意味着在onMounted的回调里你可以百分之百地确信任何通过模板引用ref绑定的 DOM 元素或子组件实例都是可访问的。这是它与onBeforeMount最核心的区别。onBeforeMount在 DOM 即将被挂载但尚未插入父容器时执行此时虽然渲染函数已被调用并生成了 VNode但真实的 DOM 还不可访问。2.3 在异步场景下的表现一个常见的误区是认为onMounted的回调函数必须是同步的。实际上Vue 并不关心你的回调是否是异步函数。你可以放心地使用async/await。import { onMounted } from vue onMounted(async () { const data await fetchSomeData() // 处理数据... })但是这里有一个极其重要的限制onMounted的注册必须在组件的同步setup阶段完成。Vue 需要在初始化组件时同步地收集所有生命周期钩子的回调函数。如果你尝试在异步操作之后才调用onMounted它可能不会被执行。错误示例import { onMounted } from vue // 假设这是一个异步函数 const setupAsync async () { await someAsyncOperation() // 错误此时 setup 的同步阶段可能已经结束 onMounted(() { console.log(这个回调可能永远不会执行) }) } // 在 setup 中调用 setupAsync()正确做法是确保所有onMounted的调用都发生在setup函数的同步代码块中。异步逻辑应该放在onMounted的回调函数内部。2.4 与 Options API 中 mounted 的对比为了更清晰地理解我们用一个表格来对比两者特性Vue 3 Composition API -onMountedVue 2/3 Options API -mounted选项引入方式从vue中导入函数作为组件选项的一个属性调用位置在setup()函数或script setup顶层在组件配置对象的根层级this上下文无this。通过闭包直接访问setup中定义的变量和函数。指向当前组件实例 (this)可访问data、methods、props等。逻辑组织可与相关响应式数据、计算属性、方法写在一起形成逻辑组合。逻辑分散在不同选项 (data,methods,mounted等)中按选项类型组织。可复用性高。可轻松将包含onMounted的逻辑提取为组合式函数。低。复用逻辑通常需要借助 mixins 或高阶组件易导致命名冲突和来源不清。TypeScript 支持极佳。天然支持类型推断无需额外类型注解。需要为组件实例定义复杂类型或使用Vue.extend/Class API体验稍逊。从表格可以看出onMounted在代码组织、类型支持和复用性上具有明显优势这也是 Vue 3 推动开发者转向 Composition API 的重要原因。3. 实战演练onMounted 的五大典型应用场景理解了基本语法我们通过几个具体的、有代表性的例子来看看onMounted在真实项目中是如何大显身手的。我会结合最新的“Vue 3 Composition API TypeScript”组合来演示。3.1 场景一访问与操作 DOM 元素这是onMounted最经典的应用。你需要使用模板引用 (ref) 来获取 DOM 元素的直接引用。template div !-- 1. 在模板中通过 ref 属性绑定引用 -- input refinputRef typetext placeholder聚焦我 / canvas refcanvasRef width400 height200/canvas /div /template script setup langts import { ref, onMounted, nextTick } from vue // 2. 声明一个与模板 ref 同名的 ref 变量 const inputRef refHTMLInputElement | null(null) const canvasRef refHTMLCanvasElement | null(null) onMounted(() { // 3. 在 onMounted 中ref.value 就是真实的 DOM 元素 if (inputRef.value) { inputRef.value.focus() // 自动聚焦输入框 console.log(输入框的值, inputRef.value.value) } if (canvasRef.value) { const ctx canvasRef.value.getContext(2d) if (ctx) { ctx.fillStyle skyblue ctx.fillRect(10, 10, 100, 100) // 在 canvas 上绘制一个矩形 } } }) /script注意有时即使你在onMounted中ref.value也可能是null例如元素被v-if条件渲染隐藏了。安全的做法是总是先进行判空。对于需要等待下一个 DOM 更新周期的情况比如在修改了响应式数据后立即操作 DOM可以结合nextTick使用。3.2 场景二初始化第三方库或插件许多第三方 JS 库如图表库、地图 SDK、富文本编辑器都需要一个真实的 DOM 容器来进行初始化。template div refchartContainer stylewidth: 600px; height: 400px;/div /template script setup langts import { ref, onMounted, onUnmounted, watch } from vue import * as echarts from echarts // 引入 ECharts const chartContainer refHTMLElement | null(null) let chartInstance: echarts.ECharts | null null // 保存图表实例 const initChart () { if (!chartContainer.value) return // 销毁旧的实例防止重复初始化 if (chartInstance) { chartInstance.dispose() } // 基于容器初始化图表 chartInstance echarts.init(chartContainer.value) // 设置图表选项 chartInstance.setOption({ title: { text: 销售趋势图 }, xAxis: { data: [一月, 二月, 三月] }, yAxis: {}, series: [{ type: line, data: [100, 200, 150] }] }) } onMounted(() { // 确保容器存在后初始化 initChart() // 监听窗口变化使图表自适应 window.addEventListener(resize, handleResize) }) const handleResize () { chartInstance?.resize() } // 重要在组件卸载时清理资源防止内存泄漏 onUnmounted(() { chartInstance?.dispose() window.removeEventListener(resize, handleResize) }) // 假设有一个响应式数据 chartData 变化时需要更新图表 const props defineProps{ chartData: Arraynumber }() watch(() props.chartData, (newData) { if (chartInstance) { chartInstance.setOption({ series: [{ data: newData }] }) } }, { deep: true }) /script这个例子展示了完整的生命周期管理在onMounted中初始化在onUnmounted中清理并结合watch响应数据变化。这是集成第三方库的标准模式。3.3 场景三发起初始数据请求在组件挂载时从服务器获取数据是最常见的需求之一。我们需要处理好加载状态、错误和数据的绑定。template div div v-ifloading加载中.../div div v-else-iferror加载失败: {{ error }}/div ul v-else li v-foritem in list :keyitem.id{{ item.name }}/li /ul /div /template script setup langts import { ref, onMounted } from vue interface ListItem { id: number name: string } const list refListItem[]([]) const loading ref(false) const error refstring | null(null) const fetchListData async () { loading.value true error.value null try { // 模拟 API 调用 const response await fetch(/api/list) if (!response.ok) throw new Error(HTTP error! status: ${response.status}) const data await response.json() list.value data } catch (err) { error.value err instanceof Error ? err.message : 未知错误 console.error(获取列表失败:, err) } finally { loading.value false } } onMounted(() { fetchListData() }) /script实操心得在实际项目中数据请求逻辑通常会被抽象到一个独立的组合式函数或Piniastore 中。onMounted里只是触发这个动作的入口。这样做的好处是数据获取逻辑可以被多个组件复用并且更容易进行单元测试。3.4 场景四建立事件监听器在组件挂载时监听全局事件如键盘事件、窗口大小变化或第三方对象的事件并在卸载时移除是防止内存泄漏的关键。script setup langts import { onMounted, onUnmounted } from vue onMounted(() { const handleKeydown (event: KeyboardEvent) { if (event.key Escape) { console.log(用户按下了 ESC 键) // 执行关闭弹窗等操作 } } document.addEventListener(keydown, handleKeydown) // 将移除监听器的函数保存下来以便在 onUnmounted 中调用 // 这是一个常见的模式确保清理函数与建立函数配对 const cleanup () { document.removeEventListener(keydown, handleKeydown) } // 注意这里不能直接写 onUnmounted(cleanup)因为 onUnmounted 也需要在 setup 同步阶段注册。 // 我们需要在 onUnmounted 的回调里调用 cleanup。 }) // 正确的清理方式在同步的 setup 阶段注册 onUnmounted onUnmounted(() { // 在这里执行清理逻辑引用上面定义的 cleanup 函数或直接写移除逻辑 // 由于 handleKeydown 是在 onMounted 内部定义的这里无法直接访问。 // 更好的模式见下面的“进阶模式”部分。 }) /script上面的写法有个问题在onMounted内部定义的事件处理函数handleKeydown在onUnmounted中无法直接引用以进行移除。更健壮的模式是使用响应式引用来保存处理函数或者使用一个自动清理的副作用函数。3.5 场景五执行一次性初始化配置有些初始化操作只需要在组件生命周期中执行一次例如设置页面标题、初始化全局状态非响应式、或向服务器发送一个组件加载的埋点。script setup langts import { onMounted } from vue import { useSomeGlobalStore } from /stores/global const globalStore useSomeGlobalStore() onMounted(() { // 1. 设置页面标题 document.title 我的Vue3应用 - ${globalStore.currentPageName} // 2. 发送一次性的分析事件埋点 if (typeof window.gtag ! undefined) { window.gtag(event, component_view, { component_name: UserProfile }) } // 3. 初始化一些只读的全局配置非响应式 if (!window.__MY_APP_CONFIG) { window.__MY_APP_CONFIG Object.freeze({ version: 1.0.0, env: import.meta.env.MODE }) } console.log(组件初始化配置完成。) }) /script这类操作通常不依赖于具体的 DOM 元素但必须在组件进入视图后才执行因此放在onMounted中是合适的。4. 进阶模式与深度避坑指南掌握了基本用法后我们来看看在实际开发中围绕onMounted有哪些高级技巧和容易踩的“坑”。4.1 组合式函数中的生命周期钩子这是 Composition API 最强大的特性之一。你可以将包含生命周期的逻辑封装成一个独立的、可复用的函数。// composables/useMousePosition.ts import { ref, onMounted, onUnmounted } from vue export function useMousePosition() { const x ref(0) const y ref(0) const updateMousePosition (event: MouseEvent) { x.value event.clientX y.value event.clientY } // 生命周期钩子被封装在组合式函数内部 onMounted(() { console.log(开始监听鼠标位置) window.addEventListener(mousemove, updateMousePosition) }) onUnmounted(() { console.log(停止监听鼠标位置) window.removeEventListener(mousemove, updateMousePosition) }) // 返回响应式数据 return { x, y } }然后在组件中使用它script setup langts import { useMousePosition } from /composables/useMousePosition const { x, y } useMousePosition() // 无需在组件中再写任何 onMounted 或 onUnmounted 来管理鼠标监听 /script template div鼠标位置{{ x }}, {{ y }}/div /template为什么可以这样当你在组合式函数中调用onMounted时它会自动绑定到调用该组合式函数的组件的生命周期。Vue 通过内部的组件实例上下文来实现这一点。这意味着你可以创建出自带生命周期管理的“黑盒”逻辑单元极大提升了代码的模块化程度。4.2 处理异步组件与 Suspense 的场景当使用异步组件 (defineAsyncComponent) 或Suspense时onMounted的调用时机需要特别注意。对于普通的异步组件onMounted会在该组件本身的挂载完成后才被调用而不是在父组件挂载时。这符合直觉。但在Suspense边界内情况略有不同。Suspense会等待其默认插槽内的所有异步依赖包括异步组件和async setup()都解析完成后才将其视为“已挂载”。因此子组件中的onMounted钩子会在Suspense的resolve事件之后才被触发。如果你的初始化逻辑依赖于父组件通过Suspense传递的异步数据这通常是正确的行为。但如果你需要执行一些不依赖异步数据的、更早的 DOM 操作可能需要重新考虑逻辑放置的位置或者使用onActivated如果组件被KeepAlive缓存。4.3 与 SSR服务端渲染的兼容性在服务端渲染 (SSR) 环境中onMounted不会被执行。因为onMounted是一个浏览器特有的生命周期钩子它依赖于 DOM 的存在。在 Node.js 服务器端没有 DOM 环境。这意味着所有在onMounted中执行的代码如访问document、window、操作 DOM、初始化依赖 DOM 的库在 SSR 过程中都会被跳过。这通常是我们期望的因为服务器只需要生成静态的 HTML 字符串。然而这引出了一个关键问题如果你的组件逻辑必须在客户端和服务器端都执行例如基于用户代理设置一些状态或者你需要在 SSR 时获取数据该怎么办对于数据获取使用专门为 SSR 设计的钩子或模式如useAsyncData(Nuxt 3)、onServerPrefetch或者在setup中执行异步逻辑但确保其能在 SSR 环境下运行不访问客户端 API。对于客户端特有操作将代码放在onMounted中是标准的做法。SSR 会跳过它在客户端激活 (hydrate) 阶段再执行。条件性访问客户端 API如果你有一段逻辑既需要在服务端运行部分又需要在客户端运行完整可以使用条件判断import { onMounted } from vue // 这段代码在 SSR 和客户端都会执行 if (typeof window ! undefined) { // 这段代码只在客户端执行 onMounted(() { // 安全的客户端 DOM 操作 }) }或者使用import.meta.env.SSR(Vite) 或process.server(Nuxt) 这样的环境变量进行判断。4.4 常见陷阱与解决方案陷阱一在 onMounted 中修改响应式数据触发无限更新有时你会在onMounted中修改一个响应式数据而这个数据又在模板中被用于一个会触发组件重新渲染的属性如v-if。如果处理不当可能导致无限循环。template div v-ifisLoaded内容已加载/div /template script setup import { ref, onMounted } from vue const isLoaded ref(false) onMounted(() { isLoaded.value true // 这通常没问题 // 但如果这里还有其它逻辑触发了额外的、非预期的重新挂载就可能有问题。 }) /script解决方案确保onMounted中的操作是幂等的或者使用标志位防止重复执行。对于复杂的副作用考虑使用watch或watchEffect来响应状态变化而不是在onMounted中直接触发可能导致状态连锁变化的操作。陷阱二忘记清理副作用导致内存泄漏这是最容易被忽视的问题。在onMounted中设置了全局事件监听器、定时器、第三方库实例如果没有在onUnmounted中清理当组件被销毁后这些引用依然存在导致内存无法被释放。// 错误示例定时器未清理 onMounted(() { setInterval(() { console.log(心跳) }, 1000) }) // 组件卸载后这个定时器会一直存在 // 正确示例 let timerId onMounted(() { timerId setInterval(() { console.log(心跳) }, 1000) }) onUnmounted(() { if (timerId) clearInterval(timerId) })解决方案养成配对编程的习惯。每一个在onMounted或onActivated中创建的副作用都必须对应一个在onUnmounted或onDeactivated中的清理操作。对于事件监听器可以使用vueuse等工具库提供的自动清理函数。陷阱三依赖未初始化的 ref 或子组件有时你可能会在onMounted中访问一个ref但这个ref绑定的是一个用v-if控制的条件渲染元素或者是一个异步子组件。在挂载瞬间这个ref可能是null。template div button clickshowInput !showInput切换/button input v-ifshowInput refconditionalInputRef / /div /template script setup import { ref, onMounted } from vue const showInput ref(false) const conditionalInputRef ref(null) onMounted(() { // 危险初始时 showInput 为 falseconditionalInputRef.value 是 null console.log(conditionalInputRef.value) // null // conditionalInputRef.value.focus() // 会报错 }) /script解决方案总是对ref.value进行判空操作。如果逻辑必须在元素存在后才能执行可以考虑使用watch来监听ref值的变化或者使用nextTick确保 DOM 更新循环结束后再执行操作。import { watch, nextTick } from vue // 方案一使用 watch watch(conditionalInputRef, (newEl) { if (newEl) { newEl.focus() } }) // 方案二在触发条件变化后使用 nextTick const handleClick async () { showInput.value true await nextTick() // 等待 DOM 更新 if (conditionalInputRef.value) { conditionalInputRef.value.focus() } }5. 测试策略如何为包含 onMounted 的组件编写单元测试为使用了onMounted的组件编写可测试的代码关键在于将生命周期钩子中的逻辑与钩子注册本身分离。我们测试的是逻辑而不是 Vue 的生命周期机制。5.1 将逻辑提取到独立的函数或组合式函数中这是最推荐的做法。onMounted里只应该包含函数调用。// 不好测试的写法 onMounted(() { const data someComplexCalculation(props.id) state.value data document.title 详情 - ${data.name} }) // 易于测试的写法 const initializeComponent (id: number) { const data someComplexCalculation(id) state.value data document.title 详情 - ${data.name} } onMounted(() { initializeComponent(props.id) })现在你可以直接导入并测试initializeComponent函数而无需模拟 Vue 组件环境或触发onMounted钩子。5.2 使用 Vue Test Utils 或 Vitest 进行组件测试当你需要测试钩子是否在正确时机被调用或者组件挂载后的整体行为时可以使用测试工具。// MyComponent.spec.ts import { mount } from vue/test-utils import MyComponent from ./MyComponent.vue import { vi } from vitest // 假设组件内部使用了 window.addEventListener describe(MyComponent, () { beforeEach(() { // 模拟全局对象 vi.spyOn(window, addEventListener) vi.spyOn(window, removeEventListener) }) afterEach(() { vi.restoreAllMocks() }) it(应该在挂载时添加事件监听器, async () { const wrapper mount(MyComponent) // 等待 nextTick 确保 onMounted 已执行 await wrapper.vm.$nextTick() expect(window.addEventListener).toHaveBeenCalledWith( resize, expect.any(Function) ) }) it(应该在卸载时移除事件监听器, async () { const wrapper mount(MyComponent) await wrapper.vm.$nextTick() wrapper.unmount() await wrapper.vm.$nextTick() expect(window.removeEventListener).toHaveBeenCalledWith( resize, expect.any(Function) ) }) })5.3 测试组合式函数如果生命周期逻辑被封装在组合式函数里测试起来会更简单。// useMousePosition.spec.ts import { renderHook, waitFor } from testing-library/vue import { useMousePosition } from ./useMousePosition import { vi } from vitest describe(useMousePosition, () { it(应该监听 mousemove 事件并更新位置, async () { const { result } renderHook(() useMousePosition()) // 模拟鼠标事件 const mouseEvent new MouseEvent(mousemove, { clientX: 100, clientY: 200 }) window.dispatchEvent(mouseEvent) // 等待响应式更新 await waitFor(() { expect(result.current.x.value).toBe(100) expect(result.current.y.value).toBe(200) }) }) })通过将逻辑与生命周期注册解耦并利用现代测试工具你可以有效地为包含onMounted的 Vue 3 代码编写可靠、可维护的单元测试。记住测试的目标是业务逻辑的正确性而不是 Vue 框架本身的行为。
返回列表