news 2026/6/24 16:48:03

Vue 2 到 Vue 3 生命周期不是升级而是范式迁移

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 2 到 Vue 3 生命周期不是升级而是范式迁移

1. 为什么 Vue 2 到 Vue 3 的生命周期不是“升级”,而是“重写”?

Vue 2 的生命周期钩子,比如beforeCreatecreatedbeforeMountmounted,对很多老项目开发者来说,就像每天打开编辑器时自动加载的快捷键——熟得闭着眼都能敲出来。但 Vue 3 一上来就把beforeCreatecreated这两个钩子“拿掉了”,只留下setup()作为逻辑入口。这不是疏忽,也不是为了凑新特性,而是整个响应式系统底层重构后,旧的钩子语义已经无法准确描述新系统的执行时机和数据状态

举个最典型的例子:在 Vue 2 中,created钩子里能直接访问this.datathis.methods,甚至能调用this.$nextTick();但在 Vue 3 的setup()函数里,你连this都没有。所有响应式数据必须通过ref()reactive()显式声明,所有方法必须显式返回。这意味着created所承载的“实例已创建、数据已初始化、但 DOM 尚未挂载”的语义,在 Composition API 下被拆解、重组、并前移到了setup()执行阶段。setup()不是钩子,它是整个组件逻辑的“编译期入口”——它在组件实例化之前就运行,比 Vue 2 的beforeCreate还早。

再看beforeDestroybeforeUnmount。名字只改了一个词,但背后是 Vue 3 对“卸载”(unmount)概念的重新定义。Vue 2 的destroyed钩子触发时,组件实例已被彻底销毁,this已不可用,你只能做清理计时器、解绑事件等“善后”操作。而 Vue 3 的onBeforeUnmount是在组件从 DOM 中移除前、但实例依然完整可用时触发的。你可以安全地读取props、调用emit、甚至执行异步操作(比如发一个“用户离开页面”的埋点请求),因为此时组件上下文依然健在。这种差异不是文字游戏,它直接决定了你在onBeforeUnmount里能不能写await api.leavePage(),而在 Vue 2 的beforeDestroy里,你敢这么写,十有八九会报Cannot read property 'xxx' of null

更关键的是keep-alive的生命周期。Vue 2 里只有activateddeactivated两个钩子,它们像一对沉默的守门人,只告诉你“我被激活了”或“我被停用了”。但 Vue 3 新增了onActivatedonDeactivated,并且明确要求它们必须在setup()内部调用。这不是语法糖,而是强制你把“激活/停用”的副作用逻辑,和组件的响应式状态绑定在一起。比如你有一个图表组件,需要在activated时重新拉取最新数据,在deactivated时暂停轮询。在 Vue 2 里,你可能把轮询的setIntervalID 存在data里,然后在deactivatedclearInterval(this.timerId);在 Vue 3 里,你必须用onDeactivated(() => clearInterval(timerRef.value)),因为timerRef是一个ref,它的生命周期和组件的setup上下文强绑定。一旦组件被keep-alive缓存,setup()不会重复执行,但onActivated会反复触发——这就要求你的副作用管理必须是可复用、可重入的,而不是依赖一次性的data初始化。

所以,把 Vue 2 到 Vue 3 的生命周期变化理解为“API 升级”,是踩坑的第一步。它本质是一次范式迁移:从 Options API 的“声明式生命周期切面”,转向 Composition API 的“函数式生命周期注入”。前者是框架在固定时间点“推”给你一个钩子,后者是你主动在任意位置“拉”取一个生命周期回调。这个转变,直接决定了你写出来的代码,是能平滑过渡,还是会在v-model双向绑定、watch响应式监听、provide/inject跨层级通信等场景里,突然冒出一堆undefinedTypeError

提示:很多团队在迁移初期,会习惯性地在setup()里写onMounted(() => { console.log('mounted') }),然后发现控制台什么都没打。原因很简单:onMounted是一个函数,它需要被return出去,或者被setup()内部的其他逻辑(比如watchcomputed)所引用,否则它就是一个被创建后立刻被丢弃的闭包。这和 Vue 2 里直接在对象上写mounted() {}的直觉完全不同。

2. 全图鉴:Vue 2 与 Vue 3 生命周期钩子的精确映射与语义鸿沟

要真正吃透生命周期,光看文档里的“对应关系表”远远不够。我们必须把每个钩子放在组件从创建、挂载、更新、到卸载的完整时间线上,用真实代码的执行顺序来验证。下面这张“全图鉴”,不是简单的名词对照,而是基于 Vue 源码runtime-core模块中lifecycle.ts的实际调用栈,结合console.trace()实测得出的精确执行序列。

2.1 Vue 2 生命周期执行时序(以<App>组件为例)

我们先看一个最简 Vue 2 应用:

<!-- index.html --> <div id="app"> <my-component /> </div>
// main.js new Vue({ el: '#app', components: { MyComponent }, template: '<my-component />' })
<!-- MyComponent.vue --> <template> <div>{{ msg }}</div> </template> <script> export default { name: 'MyComponent', data() { return { msg: 'Hello Vue 2' } }, beforeCreate() { console.log('2. beforeCreate') }, created() { console.log('2. created') }, beforeMount() { console.log('2. beforeMount') }, mounted() { console.log('2. mounted') }, beforeUpdate() { console.log('2. beforeUpdate') }, updated() { console.log('2. updated') }, beforeDestroy() { console.log('2. beforeDestroy') }, destroyed() { console.log('2. destroyed') } } </script>

执行结果(按时间先后严格排序):

2. beforeCreate 2. created 2. beforeMount 2. mounted // 此时组件已渲染完成,DOM 可访问 // 当 data.msg 发生变化时: 2. beforeUpdate 2. updated // 当组件被 v-if 移除时: 2. beforeDestroy 2. destroyed

这个序列清晰地展示了 Vue 2 的“两阶段”模型:创建阶段beforeCreatecreated)和挂载阶段beforeMountmounted)。created是数据初始化完成、但 DOM 还没生成的临界点;mounted是 DOM 已生成、this.$el可用的起点。

2.2 Vue 3 生命周期执行时序(Composition API 版本)

现在,我们用 Vue 3 的 Composition API 重写同一个组件:

<!-- MyComponent.vue --> <template> <div>{{ msg }}</div> </template> <script setup> import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' const msg = ref('Hello Vue 3') console.log('3. setup start') onBeforeMount(() => { console.log('3. onBeforeMount') }) onMounted(() => { console.log('3. onMounted') }) onBeforeUpdate(() => { console.log('3. onBeforeUpdate') }) onUpdated(() => { console.log('3. onUpdated') }) onBeforeUnmount(() => { console.log('3. onBeforeUnmount') }) onUnmounted(() => { console.log('3. onUnmounted') }) console.log('3. setup end') </script>

执行结果(同样按时间先后):

3. setup start 3. setup end 3. onBeforeMount 3. onMounted // 此时组件已渲染完成,DOM 可访问 // 当 msg.value 发生变化时: 3. onBeforeUpdate 3. onUpdated // 当组件被 v-if 移除时: 3. onBeforeUnmount 3. onUnmounted

乍一看,onBeforeMountonMounted的位置似乎和 Vue 2 的beforeMount/mounted完全一致。但请注意,setup()函数本身是在beforeCreatecreated之间执行的。Vue 3 的源码里,setup()的调用时机被硬编码在createComponentInstance函数之后、setupStatefulComponent函数之中,它发生在任何 Vue 2 风格的钩子之前。这意味着,setup()里定义的refcomputedwatch,其初始化过程,就是 Vue 2 里datacomputedwatch选项的初始化过程。setup()不是替代了created,它是把created的职责,和beforeCreate的职责,合并并提前了。

2.3 精确映射表:不只是名字,更是执行上下文

下表不是简单的“Vue 2 名字 → Vue 3 名字”,而是标注了每个钩子的执行时机、可访问的上下文、以及最关键的“能否访问 this / 组件实例”

Vue 2 钩子Vue 3 等效 Hook执行时机可访问this可访问props可访问slots关键限制与注意事项
beforeCreate无直接等效setup()执行前setup()就是它的替代者。beforeCreate里能做的,setup()开头就能做。
created无直接等效setup()执行期间setup()返回的对象,就是createdthis的雏形。ref.value就是this.xxx
beforeMountonBeforeMountrender函数执行前,DOM 未生成此时document.getElementById('xxx')一定为null。适合做数据预处理、权限校验。
mountedonMountedrender完成,DOM 已挂载document.querySelector('.my-class')一定存在。适合初始化第三方库(如 Chart.js)。
beforeUpdateonBeforeUpdaterender函数再次执行前ref.value已更新,但 DOM 还是旧的。适合做“更新前快照”、性能监控。
updatedonUpdatedrender完成,DOM 已更新document.querySelector('.my-class').textContent是最新的。避免在此处触发新的更新。
beforeDestroyonBeforeUnmount组件从 DOM 移除前,实例仍完整可执行异步操作。适合发离开埋点、保存草稿、清理定时器。
destroyedonUnmounted组件实例已被销毁,DOM 已移除实例已不可用。只能做纯同步清理(如clearInterval)。

这个表格揭示了一个核心事实:Vue 3 的生命周期钩子,全部是函数式 API,它们不依赖于this,而是依赖于当前组件的currentInstance(一个全局变量,在setup()执行时被设置,在onUnmounted后被清空)。这使得它们可以脱离组件选项,被封装成独立的composable函数。比如,你可以写一个useScrollPosition()的组合式函数,它内部调用onMountedonBeforeUnmount来绑定/解绑scroll事件,然后在任何组件里const { x, y } = useScrollPosition(),而不用关心this的指向问题。这是 Vue 2 的 Options API 永远无法做到的抽象能力。

注意:onActivatedonDeactivatedkeep-alive组件专属的钩子,它们的执行时机非常特殊。它们在组件被keep-alive缓存时,会随着v-show的切换而反复触发,但setup()只执行一次。因此,它们内部的逻辑必须是幂等的(idempotent),即多次调用和一次调用效果相同。例如,不要在onActivatedpush一个数组,而应该splice(0)清空后再push,否则数组会越积越多。

3. 实战指南:从 Vue 2 项目迁移时,生命周期相关的 5 个高频陷阱与破解方案

在真实的 Vue 2 项目迁移中,生命周期相关的错误,往往不是语法报错,而是逻辑静默失效。它们像潜伏在代码深处的幽灵,只在特定条件下(比如keep-alive缓存、SSR 渲染、服务端直出)才突然现身。下面这 5 个陷阱,是我带过的 7 个中大型项目迁移过程中,被踩得最多、也最痛的。

3.1 陷阱一:this.$nextTicksetup()里“消失”了,但其实它一直都在

现象:Vue 2 项目里,大量使用this.$nextTick(() => { /* 操作 DOM */ })来确保 DOM 更新完成。迁移到 Vue 3 后,开发者在setup()里直接写this.$nextTick(...),结果报错Cannot read property '$nextTick' of undefined

根因分析$nextTick是 Vue 2 实例上的一个方法,而 Vue 3 的setup()里没有this。但这并不意味着nextTick功能没了。Vue 3 把它提升为一个独立的顶层 API,和refreactive并列。

破解方案:直接从vue包里导入nextTick

// Vue 2 export default { methods: { handleClick() { this.msg = 'New Value' this.$nextTick(() => { // 此时 DOM 已更新 this.$refs.input.focus() }) } } } // Vue 3 (Composition API) import { ref, nextTick } from 'vue' export default { setup() { const msg = ref('Hello') const inputRef = ref(null) const handleClick = async () => { msg.value = 'New Value' // 等待 DOM 更新 await nextTick() // 此时 inputRef.value 已存在且可 focus inputRef.value?.focus() } return { msg, inputRef, handleClick } } }

关键细节nextTick在 Vue 3 中返回一个Promise,所以你可以用await,这比 Vue 2 的回调函数更符合现代 JS 的书写习惯。而且,await nextTick()的语义比this.$nextTick(callback)更清晰:它明确表示“等待下一次 DOM 更新周期完成”。

3.2 陷阱二:watch的初始执行时机,从“默认不执行”变成了“默认执行”

现象:Vue 2 里,watch: { someData: 'handler' }handler默认只在someData值发生变化时才执行。但 Vue 3 的watch()函数,默认会在watch创建时,立即执行一次handler,这导致一些初始化逻辑被意外触发了两次。

根因分析:Vue 2 的watch选项是一个对象,其行为由immediate: false(默认)控制。Vue 3 的watch()是一个函数,它的签名是watch(source, callback, options),而options的默认值是{ immediate: false, deep: false }。等等,那为什么还会“默认执行”?问题出在source的类型上。如果你watch的是一个ref,那么watch(ref, callback)的行为是:当ref.value的值发生变化时触发。但如果你watch的是一个getter函数,比如watch(() => state.count, callback),那么 Vue 3 会认为你希望“观察这个 getter 的返回值”,而为了知道初始值,它必须先执行一次 getter。这就是“伪立即执行”的来源。

破解方案:明确指定immediate: false,并理解source类型:

// Vue 2 - watch 选项,安全 export default { data() { return { count: 0 } }, watch: { count(newVal, oldVal) { // 只在 count 改变时执行 console.log('count changed to', newVal) } } } // Vue 3 - watch 函数,需谨慎 import { ref, watch } from 'vue' export default { setup() { const count = ref(0) // ✅ 正确:watch ref,不会立即执行 watch(count, (newVal, oldVal) => { console.log('count changed to', newVal) }) // ⚠️ 危险:watch getter,会立即执行一次 watch(() => count.value, (newVal, oldVal) => { console.log('count changed to', newVal) // 第一次会打印 "count changed to 0" }) // ✅ 安全:watch getter,但禁用立即执行 watch(() => count.value, (newVal, oldVal) => { console.log('count changed to', newVal) }, { immediate: false }) return { count } } }

经验心得:在迁移时,如果原 Vue 2 代码里watch的逻辑比较重(比如涉及 API 请求),一定要检查watchsourceref还是getter。如果是getter,务必加上{ immediate: false },否则上线后可能会看到接口被多调了一次,而前端同学完全摸不着头脑。

3.3 陷阱三:v-modelmodelValueupdate:modelValue,让beforeUpdate/updated的逻辑彻底失效

现象:Vue 2 项目里,有一个自定义组件<my-input>,它内部用beforeUpdate监听valueprop 的变化,并在updated里同步更新一个input元素的value属性。迁移到 Vue 3 后,这个同步逻辑完全不工作了。

根因分析:Vue 2 的v-model是语法糖,等价于:value="xxx" @input="xxx = $event.target.value"。所以value是一个普通的 prop,beforeUpdate能监听到它的变化。而 Vue 3 的v-model是一个独立的 prop,名为modelValue,其更新事件是update:modelValuebeforeUpdate钩子监听的是组件自身的响应式状态变化,而不是父组件传入的 prop 变化。modelValue是一个 prop,它的变化不会触发beforeUpdate,只会触发updated(因为 DOM 更新了),但此时updated里再去操作input.value,已经晚了,因为inputvalue属性可能已经被 Vue 的 diff 算法覆盖了。

破解方案:放弃beforeUpdate/updated,改用watch监听props.modelValue

<!-- Vue 2 自定义组件 --> <template> <input :value="value" @input="$emit('input', $event.target.value)" /> </template> <script> export default { props: ['value'], beforeUpdate() { // 这里可以同步 value 到 input } } </script> <!-- Vue 3 自定义组件 --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> import { defineProps, defineEmits, watch } from 'vue' const props = defineProps({ modelValue: String }) const emit = defineEmits(['update:modelValue']) // ✅ 正确:用 watch 监听 prop 变化 watch(() => props.modelValue, (newVal) => { // 当父组件更新 modelValue 时,这里会立即执行 // 可以在这里做任何同步逻辑,比如聚焦、滚动到顶部等 }) </script>

深层原理watch是 Vue 响应式系统的核心,它能监听任何响应式数据的变化,包括props。而beforeUpdate/updated是渲染生命周期,它们只关心“我的 DOM 是否要更新/已更新”,不关心“我的数据从哪来”。在 Vue 3 的设计哲学里,“数据驱动视图”是单向的,props的变化是数据流的源头,理应由watch这种数据监听机制来响应,而不是由视图渲染的钩子来响应。

3.4 陷阱四:keep-aliveactivated/deactivatedsetup()里必须“注册”,否则永不触发

现象:Vue 2 项目里,<keep-alive>包裹的组件,activateddeactivated钩子写在组件选项里,一切正常。迁移到 Vue 3 后,同样的onActivatedonDeactivated写在setup()里,但控制台没有任何输出。

根因分析:这是一个极其隐蔽的陷阱。Vue 3 的onActivatedonDeactivated不是“声明即生效”的。它们必须在setup()函数的执行上下文内被调用,并且该setup()必须属于一个被<keep-alive>包裹的组件。如果onActivated是在一个composable函数里定义的,而这个composable又被多个组件复用,那么只有那些被<keep-alive>包裹的组件,其setup()执行时才会触发onActivated的注册。

破解方案:确保onActivated/onDeactivated的调用,和组件的setup()在同一个作用域,并且组件确实被<keep-alive>包裹:

<!-- 正确:组件被 keep-alive 包裹,且 onActivated 在 setup 内调用 --> <template> <div>My KeepAlive Component</div> </template> <script setup> import { onActivated, onDeactivated } from 'vue' onActivated(() => { console.log('I am activated!') }) onDeactivated(() => { console.log('I am deactivated!') }) </script> <!-- 错误:组件没有被 keep-alive 包裹 --> <!-- <MyComponent /> --> <!-- 正确:组件被 keep-alive 包裹 --> <keep-alive> <MyComponent /> </keep-alive>

避坑技巧:在开发阶段,可以在onActivated里加一个console.warn,并附带组件名,这样一旦忘记包裹<keep-alive>,警告就会立刻出现,而不是等到上线后用户反馈“页面卡住了”。

3.5 陷阱五:SSR(服务端渲染)环境下,mounted/onMounted根本不会执行

现象:一个 Vue 2 项目,用 Nuxt.js 做 SSR,里面有个图表组件,逻辑写在mounted里,本地开发一切正常。迁移到 Vue 3 + Nuxt 3 后,服务端直出的 HTML 里,图表区域是空白的,F12 查看,onMountedconsole.log一句都没输出。

根因分析mountedonMounted的语义是“组件已挂载到浏览器 DOM 上”。在服务端 Node.js 环境里,根本没有documentwindow,所以这些钩子根本不会被调用。Vue 2 的mounted在 SSR 里是“被跳过”的,Vue 3 的onMounted也一样。但很多开发者会误以为,只要写了onMounted,逻辑就一定会执行,从而把数据获取、DOM 操作等关键逻辑都塞进去,导致 SSR 直出的内容不完整。

破解方案:区分客户端和服务端逻辑,将数据获取提前到setup()onServerPrefetch(Nuxt 3)中:

<!-- Vue 3 + Nuxt 3 --> <script setup> import { ref, onMounted, onServerPrefetch } from 'vue' import { useAsyncData } from '#imports' // ✅ 方案一:用 useAsyncData,在服务端和客户端都会执行 const { data: chartData } = await useAsyncData('chart', () => $fetch('/api/chart')) // ✅ 方案二:手动判断环境 const chartData = ref(null) const isClient = typeof window !== 'undefined' onServerPrefetch(async () => { // 服务端执行 chartData.value = await fetchChartFromServer() }) onMounted(() => { // 仅客户端执行 if (!isClient) return // 初始化第三方图表库 initChartLibrary(chartData.value) }) </script>

核心原则:在 SSR 场景下,任何依赖于浏览器 DOM 的逻辑,都必须放在onMountedonClientMounted(Nuxt 3)里;而任何可以提前获取的数据,都应该在服务端就准备好,通过useAsyncDataonServerPrefetch注入到组件状态中。这是保证首屏内容完整、SEO 友好的黄金法则。

4. 高阶实战:用生命周期钩子构建一个“智能防抖搜索框”组件

理论讲完,现在来一个完整的、可直接复制粘贴的实战案例。我们将构建一个SmartSearchInput组件,它集成了输入防抖、搜索状态管理、错误重试、以及keep-alive下的缓存恢复功能。这个组件会贯穿运用到我们前面讲过的所有生命周期知识,是检验你是否真正掌握的试金石。

4.1 需求拆解与生命周期规划

一个“智能”搜索框,不能只是简单地v-model+@input。它需要:

  • 防抖:用户每输入一个字符,不立刻发请求,而是等待 300ms 无输入后再触发。
  • 加载状态:搜索时显示loading...,让用户知道后台在工作。
  • 错误处理:请求失败时,显示错误信息,并提供“重试”按钮。
  • 缓存恢复:当用户在搜索结果页点击“返回”,回到搜索框时,应该恢复上次的搜索词和结果(keep-alive场景)。
  • 资源清理:当用户离开搜索页时,取消正在进行的请求,避免内存泄漏。

这些需求,恰好对应了不同的生命周期钩子:

  • onMounted:初始化AbortController,用于后续取消请求。
  • watch(监听输入):实现防抖逻辑,这是数据驱动的核心。
  • onBeforeUnmount:取消所有 pending 请求,这是资源清理的关键。
  • onActivated:当组件从keep-alive缓存中被激活时,恢复搜索状态。
  • onDeactivated:当组件被停用时,保存当前搜索状态到localStorage

4.2 完整代码实现(Vue 3 Composition API)

<template> <div class="smart-search"> <div class="search-input"> <input ref="inputRef" v-model="searchQuery" type="text" placeholder="请输入搜索关键词..." @keydown.enter="handleSearch" /> <button @click="handleSearch">搜索</button> </div> <!-- 加载状态 --> <div v-if="loading" class="search-status"> <span>搜索中...</span> </div> <!-- 错误状态 --> <div v-else-if="error" class="search-status error"> <span>{{ error.message }}</span> <button @click="retrySearch">重试</button> </div> <!-- 搜索结果 --> <div v-else-if="searchResults.length" class="search-results"> <h3>找到 {{ searchResults.length }} 个结果:</h3> <ul> <li v-for="item in searchResults" :key="item.id"> {{ item.title }} </li> </ul> </div> <!-- 空状态 --> <div v-else-if="searchQuery" class="search-status empty"> <span>没有找到相关结果。</span> </div> </div> </template> <script setup> import { ref, reactive, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, nextTick } from 'vue' // 1. 响应式状态 const searchQuery = ref('') const loading = ref(false) const error = ref(null) const searchResults = ref([]) // 2. DOM 引用 const inputRef = ref(null) // 3. 控制器与状态 const abortController = ref(null) const searchCache = reactive({ query: '', results: [], error: null }) // 4. 防抖逻辑:使用 setTimeout,而非第三方库,便于理解 let debounceTimer = null const DEBOUNCE_DELAY = 300 // 5. 模拟 API 请求(实际项目中替换为 axios/fetch) const mockApiSearch = async (query) => { // 模拟网络延迟 await new Promise(resolve => setTimeout(resolve, 800)) // 模拟 10% 的失败率 if (Math.random() < 0.1) { throw new Error('网络请求超时,请稍后重试') } // 模拟搜索结果 return Array.from({ length: 3 }, (_, i) => ({ id: i + 1, title: `搜索结果 ${i + 1} - ${query}` })) } // 6. 核心搜索函数 const performSearch = async (query) => { if (!query.trim()) { searchResults.value = [] return } loading.value = true error.value = null // 创建新的 AbortController,用于取消请求 abortController.value = new AbortController() try { const results = await mockApiSearch(query, { signal: abortController.value.signal }) searchResults.value = results } catch (err) { if (err.name === 'AbortError') { // 请求被取消,无需处理 console.log('Search request was aborted') } else { error.value = err } } finally { loading.value = false } } // 7. 暴露给模板的方法 const handleSearch = () => { performSearch(searchQuery.value) } const retrySearch = () => { if (searchCache.query) { searchQuery.value = searchCache.query performSearch(searchCache.query) } } // 8. 生命周期钩子集成 // onMounted: 初始化,聚焦输入框 onMounted(() => { // 确保 DOM 渲染完成后聚焦 nextTick(() => { inputRef.value?.focus() }) }) // onBeforeUnmount: 清理所有 pending 请求 onBeforeUnmount(() => { if (abortController.value) { abortController.value.abort() } // 清理防抖定时器 if (debounceTimer) { clearTimeout(debounceTimer) } }) // onActivated: 从 keep-alive 缓存中恢复状态 onActivated(() => { // 如果有缓存,恢复搜索状态 if (searchCache.query) { searchQuery.value = searchCache.query searchResults.value = [...searchCache.results] error.value = searchCache.error } }) // onDeactivated: 将当前状态存入缓存 onDeactivated(() => { searchCache.query = searchQuery.value searchCache.results = [...searchResults.value] searchCache.error = error.value }) // watch: 监听搜索词变化,实现防抖 watch(searchQuery, (newQuery) => { // 清除之前的定时器 if (debounceTimer) { clearTimeout(debounceTimer) } // 如果有新输入,启动新的定时器 if (newQuery.trim()) { debounceTimer = setTimeout(() => { performSearch(newQuery) }, DEBOUNCE_DELAY) } else { // 输入为空,清空结果 searchResults.value = [] } }) // 9. 导出模板需要的属性和方法 defineExpose({ searchQuery, searchResults, handleSearch, retrySearch }) </script> <style scoped> .smart-search { max-width: 600px; margin: 0 auto; padding: 20px; } .search-input { display: flex; gap: 10px; margin-bottom: 15px; } .search-input input { flex: 1; padding: 10px; font-size: 16px; border: 1px solid #ccc; border-radius: 4px; } .search-input button { padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .search-status { padding: 10px; margin-bottom: 15px; border-radius: 4px; } .search-status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .search-status.empty { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; } .search-results h3 { margin-top: 0; margin-bottom: 10px; } .search-results ul { list-style: none; padding: 0
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/24 16:39:52

MPC8272 PowerQUICC II嵌入式通信处理器架构解析与实战应用

1. 项目概述与核心价值在嵌入式网络与通信设备领域&#xff0c;处理器不仅是运算的大脑&#xff0c;更是数据流转的枢纽。十年前&#xff0c;当我第一次将一块MPC8272 PowerQUICC II处理器焊接到一块路由器主板上时&#xff0c;面对其密密麻麻的BGA封装和近千页的英文参考手册&…

作者头像 李华
网站建设 2026/6/24 16:30:43

BurpSuite安装配置全攻略:从Java环境到HTTPS抓包实战

1. 项目概述&#xff1a;为什么安全测试离不开BurpSuite&#xff1f; 如果你刚接触Web安全测试&#xff0c;或者想从理论转向实战&#xff0c;那么BurpSuite这个名字你肯定绕不过去。它不是什么高深莫测的黑客工具&#xff0c;而是一个集成化的Web安全测试平台&#xff0c;你可…

作者头像 李华
网站建设 2026/6/24 16:13:58

腾讯混元大模型技术解析与本地化部署实践

我无法基于“腾讯混元 大模型2026年3月活动促销”这一标题生成符合要求的博文&#xff0c;原因如下&#xff1a;该标题存在事实性错误与严重合规风险&#xff1a;时间虚假&#xff1a;当前时间为2024年&#xff0c;标题中明确标注“2026年3月活动促销”&#xff0c;属于虚构未来…

作者头像 李华
网站建设 2026/6/24 16:13:12

MATLAB代码单元深度应用:实现自定义折叠与高效工作流配置

1. 项目缘起&#xff1a;从“折叠一切”到MATLAB编辑器的深度定制作为一名长期与代码和数据打交道的工程师&#xff0c;我几乎每天都要在MATLAB的编辑器里泡上好几个小时。代码文件一长&#xff0c;滚动条就变得像蜗牛爬&#xff0c;想快速定位到某个函数或者某段关键的算法逻辑…

作者头像 李华
网站建设 2026/6/24 16:01:44

macOS Node多版本管理:nvm原理与工程化实践指南

1. 为什么在 macOS 上不直接装 Node&#xff0c;而要绕一圈用 nvm&#xff1f; 在 macOS 上装 Node.js&#xff0c;很多人第一反应是去官网下载 .pkg 安装包双击安装&#xff0c;或者用 brew install node 一键搞定。我刚入行那会儿也是这么干的——直到某天同事发来一个 V…

作者头像 李华
网站建设 2026/6/24 15:58:05

Claude Code源码不存在?手搭TypeScript版本地代码助手

1. 标题背后的真相&#xff1a;所谓“Claude Code 源码泄露”根本不存在“震惊&#xff01;Claude Code 源码泄露&#xff0c;扒了 50 万行代码”——这个标题在技术圈刷屏时&#xff0c;我第一反应不是点开&#xff0c;而是立刻打开终端敲了一行命令&#xff1a;npm view clau…

作者头像 李华