news 2026/9/26 20:18:34

Vue钩子函数从入门到实战:生命周期、路由守卫与组合式API详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue钩子函数从入门到实战:生命周期、路由守卫与组合式API详解

我第一次跟人解释 Vue 的时候,最怕的场面就是对方盯着那张生命周期图发呆。图本身并不复杂,但它摆在新手面前,就像一张陌生城市的地铁线路图——你不需要记住每一站,只需要知道你要在哪下车。钩子函数(Hook)就是这个“下车点”。

很多零基础的人一听到“钩子函数”四个字就开始紧张,觉得这是个高深的概念。我打赌,你其实已经见过钩子函数了,只是没人告诉过你它的名字。你在一段代码里写过created() {},或者mounted() {},这就是钩子函数。它是 Vue 给你开的“后门”,让组件在生命周期、路由跳转、指令绑定的关键时刻,有机会执行你自己的代码。

这篇文章就是写给这些“零基础但真想弄明白”的人的。我不打算让你背任何概念,我会用你身边的事打比方,把 Vue 的钩子函数拆开揉碎讲清楚。你会知道它解决什么问题、都长在哪里、怎么用、踩过哪些坑,之后再看那些动不动讨论“生命周期”“路由守卫”的同事,你就不会心虚了。

1. 钩子函数到底是个什么东西:先别背,先理解

1.1 一次“到点叫你”的机制

设想你早上设置了闹钟。你不关心闹钟内部怎么走时,不关心它靠什么电路驱动,你只关心一件事:到点它必须响,响了你就起床。在这个例子里,闹钟就是框架,起床动作就是你写好的函数。Vue 的钩子函数也是这个逻辑:框架在固定的时间点,主动叫你一声,你提前把一个函数挂在那里,它到点就会执行。

Vue 这类前端框架的工作方式,和你手动写原生 JavaScript 很不一样。原生代码里,几乎每一行都是“你主动调用某个函数”,比如你要操作 DOM,你得自己去getElementById再改内容,一切靠自觉。Vue 不一样,它接管了组件的创建、渲染、更新、销毁的整个过程,但它又不可能知道你每个阶段想干什么,所以它提供了一堆“插槽”,让你把自己想要执行的代码填进去。这些插槽就是钩子函数。

所以钩子函数的核心,不是“我怎么调用它”,而是“框架在什么时刻调用我”。只要把这件事想明白,后面所有钩子你都能举一反三。

1.2 钩子函数不是普通方法,是框架的“主动联系”

我见过很多新手把钩子函数和methods里的普通方法弄混。两者的表面很像,都是一段函数,但触发方式天差地别。

methods里的handleClick,是“你主动调用的方法”。要么你在模板里写@click="handleClick",让用户点击时触发;要么你在代码里写this.handleClick(),自己调它。总之,不点不调,它就躺在那里不动。

钩子函数刚好反过来,是框架主动联系你。你写了mounted() {},当 Vue 把组件挂载到页面上之后,不用你喊,它自己就跑了。你写了beforeUnmount() {},当组件即将从页面移除时,哪怕你什么都没干,它也会自己执行一遍。你可以把框架想象成一个特别负责的管家:到了关键节点,管家主动通知你,你只需要提前把“待办事项”交给它。

理解这个“谁主动”的区别特别重要,因为很多排查思路都从这里延伸。如果你发现某个钩子没执行,第一反应应该是“框架认为的那个时刻到了吗”,而不是“我是不是忘了调它”。

1.3 你在哪些地方会遇到钩子函数

钩子函数不是一个孤立的、只存在于组件里的东西。它能出现在 Vue 的各个关键环节,我按出现频率给你列一下,之后你会逐一陪它们打交道:

  • 组件生命周期钩子:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted。这是最常见的一类,后面我会重点讲。
  • 路由钩子:全局的路由守卫beforeEach、afterEach,路由内独享的beforeEnter,以及组件内的beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。它们管的是“页面跳转时”这个场景。
  • 自定义指令钩子:Vue 3 中自定义指令也有一组生命周期方法,比如mounted、updated、unmounted。你写过v-focus这种指令时就会用到它们。
  • 组合式 API 里的生命周期函数:onMounted、onUpdated、onBeforeUnmount等,它们其实是组件生命周期钩子的组合式写法。
  • 第三方库或框架提供的 hooks:比如useRouter、useStorage这类以use开头的函数,本质上是把一组逻辑(很可能会用到上述钩子)封装起来,命名习惯来自社区,但底层靠的还是 Vue 自己的钩子体系。

你会不会觉得这些东西太多了记不住?不用急。它们背后的“到点叫你”机制一模一样,你只要吃透组件生命周期钩子,其他全是变体。

2. 组件生命周期钩子:Vue 最核心的那一串“到点提醒”

2.1 从创建到销毁的七个关键时间点

组件生命周期,就是 Vue 从“拿到你这段配置”到“把它彻底从页面上摘除”的完整过程。这个过程被分成了几个节点,Vue 在每个节点前后都给你安排了一个钩子。我一个个说,你跟着走一遍:

beforeCreate(创建之前):这个时刻,组件实例刚被创建,但什么都还没有。data还没有变成响应式数据,methods还不存在,this上也基本拿不到什么有用的东西。我在项目里几乎不会在这写业务逻辑,通常只是做极早期、和数据无关的初始化。

created(创建完成):到了这里,data已经变成响应式,methods、computed、watch也都能用了。但由于组件还没有渲染成真实 DOM,this.$el是不存在的,你也拿不到页面上的元素。这是发起接口请求的好时机之一,因为数据越早拿到,页面渲染时就越少等。

beforeMount(挂载之前):模板已经编译完成,但还没有真正插到页面里。它和created相隔极短,实践中很少单独用它,你甚至可以先把精力放在created和mounted上。

mounted(挂载完成):组件对应的 DOM 已经渲染进页面,你现在可以去操作this.$refs、读取元素尺寸、初始化需要 DOM 的第三方库。这是新手最喜欢的一个节点,因为“能碰到页面”了,很多逻辑都习惯堆在这里。

beforeUpdate(更新之前):数据发生变化,Vue 准备重新渲染 DOM,但页面上还是老样子。在这个钩子里,你读到的是更新前的 DOM 和数据,可以用来记录变化前的状态。

updated(更新完成):数据变化引起的重渲染已经完成,DOM 是新的了。这里有一个常见警告:不要在updated里直接改数据,否则很容易触发新一轮更新,然后又进入updated,形成死循环。

beforeUnmount(卸载之前):组件即将被销毁,此时所有实例属性仍然可用。这里最典型的用途是清理定时器、移除window上的事件监听、取消订阅。

unmounted(卸载完成):组件已经销毁,事件监听被移除,实例上的东西不能再依赖了。很多人在beforeUnmount里就把清理工作做完了,所以unmounted使用频率没那么高。

我把这些节点画成一张对比表,你以后回来看会更快:

钩子函数时机能不能访问 data能不能操作 DOM常见用途
beforeCreate实例初始化前不能不能极早期逻辑
created实例初始化后能不能请求接口、初始化数据
beforeMount渲染前能不能少见
mounted渲染后能能操作 DOM、初始化插件
beforeUpdate数据变化重渲染前能能(旧 DOM)更新前状态记录
updated重渲染后能能需要新 DOM 的后置逻辑
beforeUnmount销毁前能能清理定时器、解绑事件
unmounted销毁后不能依赖不能收尾

2.2 选项式与组合式 API 的钩子写法对照

Vue 2 时代大家写选项式 API,钩子函数就是组件配置里的一个属性。Vue 3 推出组合式 API 之后,钩子有了新的写法:原来created里做的事,基本可以挪到setup()里做;原来mounted里做的事,用onMounted()注册。我分别写给你看。

选项式写法长这样:

export default { data() { return { count: 0 } }, created() { console.log('组件创建了,可以请求数据') }, mounted() { console.log('DOM 挂载完成', this.$refs.box) }, beforeUnmount() { console.log('组件要卸载了,清理事件') } }

组合式写法长这样:

<script setup> import { ref, onMounted, onBeforeUnmount } from 'vue' const count = ref(0) console.log('相当于 created,setup 一进来就执行') onMounted(() => { console.log('DOM 挂载完成') }) onBeforeUnmount(() => { console.log('组件要卸载了') }) </script>

两种写法对应关系大致是:setup本身几乎覆盖了原来beforeCreate和created的场景,onMounted对应mounted,onUpdated对应updated,onBeforeUnmount对应beforeUnmount。实际项目里经常看到组合式和选项式混合使用,比如.vue文件里既有<script setup>,又有defineOptions配置名称。这种混用在过渡期很常见,你不用觉得奇怪。关键是你得清楚:钩子的“触发时刻”没有变,变的只是写在哪里、怎么注册。

2.3 新手最容易踩的三个细节坑

第一个坑:以为created里能操作 DOM。现实是组件还没挂载,页面元素根本不存在。你写document.querySelector也许能碰巧拿到页面其他元素,但拿不到当前组件的$refs。想要 DOM,就得到mounted之后或者用nextTick。

第二个坑:在updated里改数据。想象一个循环:数据变了,Vue 重渲染,重渲染完触发updated,你在updated里又把数据改了,Vue 又重渲染……页面卡死、控制台疯狂报错,大概率就是这个问题。如果确实需要根据更新后的 DOM 做些事,一定要先判断数据是否真的变化,再做有限度的操作。

第三个坑:把mounted当成“所有子组件都好了”的标志。父组件的mounted只代表父组件自身挂载完成,它的子组件未必全部加载完毕。如果你在父组件的mounted里去访问子组件内部的 DOM 或者依赖子组件状态,很可能会踩空。这种情况可以用this.$nextTick,或者干脆把逻辑放在子组件自己的mounted里。

3. 路由钩子:页面跳转时的那扇“暗门”

3.1 全局路由守卫:所有人过安检

如果说组件生命周期管的是“组件出生到死亡”,那路由钩子管的就是“从一个页面跳到另一个页面”的整个过程。Vue Router 在跳转的前后,也提供了钩子,让你能插一脚。

最常用的是beforeEach。每次路由跳转前,它都会先执行。我项目里最典型的使用场景就是登录鉴权:用户访问一个需要权限的页面,先在这里判断有没有登录态,没有就重定向到登录页;有就放行。再加上标题修改、进度条开启,基本一进项目就能看到它的身影。afterEach则在跳转已经确认完成后执行,我通常用它来关闭进度条、做埋点统计。还有一个beforeResolve,它在所有组件内的守卫和异步路由组件都解析完成后触发,比beforeEach更晚一些,适合做“确保一切就绪后再进入”的检查。

全局守卫和组件生命周期钩子最大的不同是:它不针对某一个组件,而是守着整个路由系统。你可以理解成“入口处人人安检”,所有页面跳转都逃不过。

3.2 组件内路由钩子:我自己管我的进出

除了全局守卫,组件自己还可以声明三个路由钩子:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。

beforeRouteEnter在路由进入这个组件前执行,但有一个让新手摸不着头脑的特点:这时候组件实例还没创建,所以拿不到this。你想访问组件数据怎么办?可以通过next的回调,等实例创建后再处理。

beforeRouteUpdate特别要画个重点:当你在同一个组件里,只是参数变化时,Vue 会复用组件实例,不会重新走created。比如从/user/1跳到/user/2,页面组件还是那个,created不会再次执行。这时你就要靠beforeRouteUpdate捕获参数变化,重新请求数据。这个问题在带参数的动态路由里几乎必踩。

beforeRouteLeave是离开这个页面时触发的。我常用它做两件事:一是表单还没保存,提示用户是否离开;二是记录用户离开时的滚动位置,下次回来能还原。注意,异步操作在这里要谨慎,因为如果用户确认离开,组件很快会被销毁,有些异步结果可能就没意义了。

3.3 一个动态路由参数更新的实战例子

我写一个非常常见的小案例:用户详情页路径为/user/:id,从用户列表点进/user/1,再点另一个联系人跳到/user/2。新手常犯的错误是把请求逻辑放在created里,结果跳到/user/2时页面内容没更新。

正确做法是给组件加上beforeRouteUpdate:

<script setup> import { ref } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const userId = ref(route.params.id) async function loadUser(id) { // 假设这里调用接口拉用户详情 userId.value = id console.log('重新请求用户:', id) } // 第一次进入,在 setup 里直接拉一次 loadUser(route.params.id) // 后续参数变化时,靠这个钩子重新拉 export default { beforeRouteUpdate(to, from, next) { loadUser(to.params.id) next() } } </script>

这里用到了<script setup>和普通导出混合的写法,虽然看起来有点怪,但在某些场景下是允许的。核心思路是:把“参数变化后要做什么”放进beforeRouteUpdate,这样从/user/1跳到/user/2时,就不会出现页面空白或者数据还是旧的情况。

4. 自定义指令钩子与组合式 hooks:钩子的两处延伸

4.1 自定义指令里的生命周期钩子

组件有生命周期,指令也有。Vue 3 中自定义指令提供了一组钩子,名字和组件生命周期很像:created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted。

看到这里你应该有感觉:反正就是“在关键节点叫我一下”。我写过最简单的v-focus,在mounted钩子里让元素自动聚焦:

const vFocus = { mounted(el) { el.focus() } }

稍微实用一点的,比如权限指令v-permission。它可以在mounted时拿到绑定值,比如按钮需要的权限码,再判断当前用户有没有这个权限,没有就直接把元素从 DOM 里移除。再比如防抖指令,可以在beforeUnmount时清理定时器。这些指令钩子解决的,都是“一个 DOM 元素在生命周期各阶段,你要怎么管它”的问题。

你也可以留意到,指令钩子函数里常常提到vnode。vnode就是虚拟 DOM 节点,你可以把它理解成“元素的说明书”,里面保存着元素的各种信息。普通业务里你可能用不到它,但当你写高级指令或者封装底层组件时,它就会出现。很多新手看到vnode就懵,其实它只是一个普通参数,你不用过度紧张。

4.2 组合式 hooks:把钩子包一层再复用

除了 Vue 自带的钩子,社区里经常提到“hooks”。比如有人会问:为什么老看到useSomething()这种函数?这就是把钩子包进一个函数里,方便多个组件复用逻辑。

我写一个简单的鼠标位置追踪函数给你看:

// useMousePosition.js import { ref, onMounted, onBeforeUnmount } from 'vue' export function useMousePosition() { const x = ref(0) const y = ref(0) function update(e) { x.value = e.clientX y.value = e.clientY } onMounted(() => window.addEventListener('mousemove', update)) onBeforeUnmount(() => window.removeEventListener('mousemove', update)) return { x, y } }

组件里任何地方要用鼠标位置,只需要:

<script setup> import { useMousePosition } from './useMousePosition' const { x, y } = useMousePosition() </script>

你看,onMounted和onBeforeUnmount这些钩子还是在工作,只是被封装进了useMousePosition。以后你看到项目里一堆use开头的函数,别再觉得神秘,它们就是“钩子的工具化封装”。这也解释了你为什么经常听到“组合式 API”和“hooks”这两个词被放在一起聊:组合式 API 的灵活性,恰恰来自钩子可以被自由组合、封装、复用。

4.3 这些钩子和组件生命周期钩子的关系

自定义指令钩子和组件生命周期钩子虽然名字相似,但它们是两套独立机制。指令挂在组件里的某个 DOM 元素上,组件销毁时,指令的unmounted也会跟着触发;组件生命周期钩子管的是整个组件的活着和死亡。两者在时间上往往重叠,但职责不同。组合式 hooks 则是在组件生命周期钩子基础上的二次封装,本质是组件钩子的消费者。所以你要是把组件生命周期钩子那套“到点叫你”的机制理解透了,后面这堆东西都不用死记,看一遍代码就能猜个大概。

5. 踩坑实录与调试建议

5.1 四大经典问题速查表

我把自己在项目里碰到的、以及带新人时反复讲的几个问题整理成了速查表,你可以直接收藏:

问题原因解决办法
接口请求写在 mounted 里,感觉数据加载慢请求可以更早发起首选在created或setup里请求,mounted 只做 DOM 相关操作
mounted 里操作 ref 拿到的是 null对应 DOM 是条件渲染,当时还没出现用v-if保证元素存在后再操作,或者用nextTick包一层
从 /user/1 跳 /user/2,页面数据不更新组件被复用,created 不会再次执行使用beforeRouteUpdate监听参数变化并重新请求
updated 里改数据导致无限循环更新后改数据又触发更新尽量避免;确实要改,加条件判断,避免每次 updated 都改
keep-alive 缓存组件后,mounted 不再重复执行组件被缓存了,不再重新挂载使用activated、deactivated这两个钩子处理缓存期间的进入和离开
组件销毁后定时器还在跑忘记清理定时器在beforeUnmount或onBeforeUnmount里清理

这里我特别说一下keep-alive。很多项目会用<keep-alive>缓存页面,比如列表页跳详情页再返回时,希望列表状态不丢。有了缓存之后,组件第一次进入会走mounted,但第二次进入就不会再走mounted了,因为组件没被销毁,只是被隐藏了。这时候你想“每次进入都做点事”,就得用activated。这是一个非常容易让人困惑的点:明明钩子函数写得没问题,为什么第二次不执行了?先看看是不是keep-alive在作怪。

5.2 DevTools 与日志:怎么确认钩子到底跑没跑

排查钩子相关问题,最土但最有效的方法是打日志。我以前带人的时候,总让他们先在每个钩子里加一行console.log,看输出顺序。比如:

created() { console.log('[created]', this.someData) } mounted() { console.log('[mounted] DOM ready') } beforeUpdate() { console.log('[beforeUpdate]') } updated() { console.log('[updated]') }

对比一下输出日志的时间顺序,你就知道 Vue 的流程走到哪了。如果你看到[created]打了两遍,多半是组件被创建了多次,比如v-if反复切换、key 变了导致重建。这种信息对定位问题特别有帮助。

Vue DevTools 也值得装。它会显示组件树、组件的 data 状态、路由信息,还能查看事件。最新版本的 DevTools 里也有时间线面板,能看到组件挂载、更新、卸载的大致时间点。不过说句实话,实战里定位钩子问题,日志还是最直接的,“跑没跑”“按什么顺序跑”“参数是什么”,一眼就穿。

5.3 我在项目里用钩子函数的几个习惯

最后分享几条我现在写代码的习惯,都是踩过坑换来的经验。

第一,初始化数据尽量靠前,操作 DOM 尽量靠后。接口请求、数据初始化放created或setup,早一点发起请求,页面渲染完成后就能直接出数据;DOM 操作、第三方库初始化放mounted,等元素真实存在了再动。

第二,能清理的一定要清理。只要在钩子里绑定了window.addEventListener、开了setInterval,请务必在beforeUnmount或onBeforeUnmount里移除和关闭。你可能觉得页面关了无所谓,但在单页应用里,组件销毁后监听还在,内存泄漏往往就是这么积累出来的。

第三,不要在钩子里塞一坨太厚的逻辑。钩子函数适合做“时机控制”,具体事情尽量抽成函数。比如mounted里只写一句this.initChart(),后面怎么初始化图表是另一个函数的事。这样排查问题的时候,你能很快定位“是时机不对,还是函数内部出错”,而不是面对一个几百行的mounted发呆。

第四,遇到“钩子没执行”的情况,先问自己三个问题:这个组件是不是被keep-alive缓存了?这个路由是不是复用了同一个组件?这个 DOM 是不是还没被渲染出来?这三个问题能解决掉我遇到过的绝大多数离奇现象。剩下的,就老老实实加日志,让控制台告诉你答案。把钩子函数当成一个普通工具,不神化它,也不忽视它,你会发现 Vue 很多难题其实都是从这里解开的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 20:17:58

深入拆解 client-go:Kubernetes 控制器开发的核心机制与实战指南

1. 为什么搞 Kubernetes 自动化必须先啃透 client-go如果你写过 Kubernetes 控制器、Operator 或者任何跟集群自动化沾边的工具&#xff0c;大概率已经跟 client-go 打过照面。这个库是 Kubernetes 官方维护的 Go 客户端&#xff0c;几乎所有周边生态——从 kubectl 这样的命令…

作者头像 李华
网站建设 2026/9/26 20:17:55

Linux网卡多队列调优:Intel netconf命令配置RSS哈希与Flow Director实战

聊Linux网络命令&#xff0c;大家习惯性想到ping、ss、ethtool、ip这几位“常客”&#xff0c;可一旦涉及Intel网卡驱动里的高级特性&#xff0c;比如RSS多队列哈希、Flow Director精确分流、DCB流量控制&#xff0c;常规工具基本帮不上忙。我今天要聊的&#xff0c;是藏在Inte…

作者头像 李华
网站建设 2026/9/26 20:17:24

StackEdit v5.14.10 离线版:零网络依赖的本地 Markdown 编辑器

简介&#xff1a;StackEdit v5.14.10 是一款开箱即用的纯前端 Markdown 编辑器&#xff0c;面向开发者、技术博主、学生等需高效撰写结构化文档的用户&#xff0c;解决本地无安装依赖、跨设备即时访问 Markdown 编辑环境的核心需求。资源为 RAR 压缩包&#xff08;6.96MB&#…

作者头像 李华
网站建设 2026/9/26 20:17:07

MinGW-w64 GCC 12.2.0 在 Windows 下的 C/C++ 环境配置与避坑实践

简介&#xff1a;MinGW GCC 12.2.0 是面向 Windows 64 位平台的完整 GCC 编译工具链&#xff0c;适合希望在 Windows 上编写 C/C 且不想依赖 Visual Studio 的开发者使用。该版本支持 C20 等新标准&#xff0c;可与 CMake 配合完成跨平台构建&#xff0c;并提供 gcc、g 及配套调…

作者头像 李华
网站建设 2026/9/26 20:15:12

Java Web入门必练:JDBC+Servlet+JSP实现部门增删改查系统

学 Java 到第 14 天&#xff0c;终于不再是照着教程敲语法 demo 了。这篇博文记录的是一套完整的“部门系统”案例开发过程&#xff0c;核心功能就是部门的增删查改。选它当第一个完整案例&#xff0c;是因为它足够小&#xff0c;却能覆盖 Java Web 开发里最要命的一整条链路&a…

作者头像 李华