ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

Vue钩子函数从入门到实战:生命周期、路由守卫与组合式API详解 我第一次跟人解释 Vue 的时候最怕的场面就是对方盯着那张生命周期图发呆。图本身并不复杂但它摆在新手面前就像一张陌生城市的地铁线路图——你不需要记住每一站只需要知道你要在哪下车。钩子函数Hook就是这个“下车点”。很多零基础的人一听到“钩子函数”四个字就开始紧张觉得这是个高深的概念。我打赌你其实已经见过钩子函数了只是没人告诉过你它的名字。你在一段代码里写过created() {}或者mounted() {}这就是钩子函数。它是 Vue 给你开的“后门”让组件在生命周期、路由跳转、指令绑定的关键时刻有机会执行你自己的代码。这篇文章就是写给这些“零基础但真想弄明白”的人的。我不打算让你背任何概念我会用你身边的事打比方把 Vue 的钩子函数拆开揉碎讲清楚。你会知道它解决什么问题、都长在哪里、怎么用、踩过哪些坑之后再看那些动不动讨论“生命周期”“路由守卫”的同事你就不会心虚了。1. 钩子函数到底是个什么东西先别背先理解1.1 一次“到点叫你”的机制设想你早上设置了闹钟。你不关心闹钟内部怎么走时不关心它靠什么电路驱动你只关心一件事到点它必须响响了你就起床。在这个例子里闹钟就是框架起床动作就是你写好的函数。Vue 的钩子函数也是这个逻辑框架在固定的时间点主动叫你一声你提前把一个函数挂在那里它到点就会执行。Vue 这类前端框架的工作方式和你手动写原生 JavaScript 很不一样。原生代码里几乎每一行都是“你主动调用某个函数”比如你要操作 DOM你得自己去getElementById再改内容一切靠自觉。Vue 不一样它接管了组件的创建、渲染、更新、销毁的整个过程但它又不可能知道你每个阶段想干什么所以它提供了一堆“插槽”让你把自己想要执行的代码填进去。这些插槽就是钩子函数。所以钩子函数的核心不是“我怎么调用它”而是“框架在什么时刻调用我”。只要把这件事想明白后面所有钩子你都能举一反三。1.2 钩子函数不是普通方法是框架的“主动联系”我见过很多新手把钩子函数和methods里的普通方法弄混。两者的表面很像都是一段函数但触发方式天差地别。methods里的handleClick是“你主动调用的方法”。要么你在模板里写clickhandleClick让用户点击时触发要么你在代码里写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也都能用了。但由于组件还没有渲染成真实 DOMthis.$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(相当于 createdsetup 一进来就执行) onMounted(() { console.log(DOM 挂载完成) }) onBeforeUnmount(() { console.log(组件要卸载了) }) /script两种写法对应关系大致是setup本身几乎覆盖了原来beforeCreate和created的场景onMounted对应mountedonUpdated对应updatedonBeforeUnmount对应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时页面内容没更新。正确做法是给组件加上beforeRouteUpdatescript 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 很多难题其实都是从这里解开的。
返回列表