news 2026/9/14 16:08:43

Vue 3 实战进阶:10个避免踩坑的高效技巧与性能优化指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3 实战进阶:10个避免踩坑的高效技巧与性能优化指南

写这篇文章之前,我先说明一个背景。最近团队在招前端,面试里问了不下二十个候选人关于 Vue 3 组合式 API 的用法,发现一个很有意思的现象:很多人能背出refreactivecomputed的定义,但一落到真实业务场景,要么把computed当普通函数在每个模板表达式里重复调用,要么在watch里写出死循环,要么组件传参还在用 Vue 2 时代那一整套props + $emit的冗长写法。

不是说这些用法错了,而是 Vue 3 提供了一套更顺手、更不容易出错的写法,很多人没花时间去梳理。这篇东西不是面试八股文,是我自己在真实项目里反复用、反复踩坑之后沉淀下来的 10 个技巧。覆盖响应式、组件通信、渲染性能、生命周期、逻辑复用这几块日常开发最高频的场景。新手看了能少走弯路,有一定经验的也能对照检查一下自己的代码有没有可以优化的地方。

1. computed 缓存:别再把计算属性当普通函数用

先看一个最常见的反例。列表页渲染时,很多人习惯这么写:

<template> <div v-for="item in list" :key="item.id"> {{ formatPrice(item.price) }} </div> </template> <script setup> import { ref } from 'vue' const list = ref([...]) function formatPrice(price) { // 做一些复杂计算 return '¥' + price.toFixed(2) } </script>

这个写法功能上没错,问题出在性能上。methods 里的函数在每次组件重新渲染时都会被重新执行,不管它的依赖有没有变化。也就是说,哪怕你只是修改了一个完全不相关的响应式变量触发了重渲染,所有formatPrice(item.price)也会全部重新算一遍。数据量小无所谓,数据量一大,卡顿感就来了。

computed的核心价值不是"计算",而是缓存。它内部维护了一个依赖收集机制,只有它读取的响应式数据发生变化时才会重新计算,否则直接返回上一次的结果。

<script setup> import { ref, computed } from 'vue' const list = ref([...]) const formattedList = computed(() => { return list.value.map(item => ({ ...item, priceText: '¥' + item.price.toFixed(2) })) }) </script> <template> <div v-for="item in formattedList" :key="item.id"> {{ item.priceText }} </div> </template>

这样改完之后,只有当list变化时才会重新执行计算逻辑。这里有一个很多人忽略的点:computed 里读到的每一个响应式数据都会被记录为依赖。比如 computed 里同时用了listcurrency,那这两个任何一个变了,computed 都会重新计算。

对比项computedmethods 中的函数
缓存有,依赖不变不重新执行无,每次渲染都重新执行
适用场景根据响应式数据推导新值事件处理、不需要缓存的逻辑
副作用不应有副作用可以有
模板中调用{{ computedProp }}{{ method() }}

还有一个很容易被忽视的细节:computed 里不要写副作用。比如在 computed 里修改其他响应式变量、调用接口、操作 DOM,这些都是反模式。computed 的设计初衷是纯函数——输入响应式数据,输出计算结果。如果你在 computed 里改数据,很容易造成循环更新的死循环,而且排查起来非常痛苦。

我自己在项目里定的规矩是:模板里凡是涉及数据转换、筛选、格式化的逻辑,一律用 computed;只有事件回调里才用普通函数。这个习惯养成之后,列表页面的性能问题至少少了一半。

2. watch 的正确姿势:immediate、flush 与 deep 的三重坑

watch是日常开发里用得最多的 API 之一,也是坑最多的。"为什么我的 watch 不触发?""为什么我的 watch 疯狂触发?""为什么拿不到旧值?"——这三个问题我几乎每周都能在群里看到。

第一个坑是immediate选项。默认情况下,watch 只在被监听的源发生变化后才触发,初始化时不执行。但很多业务场景需要"初始化时就跑一次",比如进入页面时根据路由参数拉取列表、根据用户信息初始化表单。

// 错误示范:进入页面时不执行,只有 searchKey 改变后才执行 watch(searchKey, () => { fetchList() }) // 正确写法:进入页面立即执行一次 watch(searchKey, () => { fetchList() }, { immediate: true })

第二个坑是deep选项。开发中经常监听一个对象,比如表单对象:

const form = reactive({ name: '', age: 0 }) // 这样写不生效!form 对象的地址没变,只变了内部属性 watch(form, () => { console.log('form 变化了') })

对于reactive对象,watch 默认是深层的,所以上面其实会触发。真正的坑在监听ref对象时:

const form = ref({ name: '', age: 0 }) // 这样写不生效!form.value 的地址没变,只变了内部属性 watch(form, () => { console.log('form 变化了') }) // 需要加 deep watch(form, () => { console.log('form 变化了') }, { deep: true })

deep: true是有代价的——它会递归遍历对象的所有属性,嵌套越深,性能开销越大。一个包含图片 base64、富文本内容的表单对象,用 deep watch 会导致明显的性能下降。

更优的做法是监听一个 getter,只关注你真正关心的字段:

watch( () => form.value.name, (newVal, oldVal) => { console.log('name 变化了', newVal, oldVal) } )

这样既精准又高效,而且能拿到oldVal。很多人在 deep watch 里拿不到旧值,因为拿到的是同一个对象引用——这也是选择 getter 监听的重要原因之一。

第三个坑是flush选项。默认情况下 watch 的回调在组件更新之前触发,这意味着你在回调里访问 DOM 时,DOM 可能还没更新完。如果你需要在 DOM 更新后再执行某些操作,就要用flush: 'post'

watch(source, async () => { // DOM 还没更新 console.log(document.querySelector('.item')?.offsetHeight) }, { flush: 'post' })

flush: 'post'相当于 Vue 2 里的$nextTick包裹效果,回调延迟到 DOM 更新之后执行。这个选项在操作 DOM 尺寸、滚动位置等场景里非常有用。

3. 响应式丢失的根源:reactive 解构与 toRefs 的正确使用

这个问题的典型场景长这样:

import { reactive } from 'vue' const state = reactive({ count: 0, name: 'Vue' }) // 解构出来的 count 和 name 变成了普通变量 const { count, name } = state count++ // 页面上的 count 不会更新

很多人第一次遇到这个问题时一脸懵:明明 state 是响应式的,怎么解构出来就不响应了?

原因在于reactive返回的是 Proxy 对象,它的响应式能力来自 Proxy 的get/set拦截。当你解构时,实际执行的是读取属性值然后赋值给一个新变量,这个新变量跟 Proxy 之间不再有任何联系。

解决这个问题有两个工具:toRefstoRef

import { reactive, toRefs } from 'vue' const state = reactive({ count: 0, name: 'Vue' }) // toRefs 把每个属性都转成 ref,解构后依然保持响应式 const { count, name } = toRefs(state) count.value++ // 这次页面会更新 // name.value 同理

toRef则适用于只取一个属性的场景:

import { reactive, toRef } from 'vue' const state = reactive({ count: 0, name: 'Vue' }) const countRef = toRef(state, 'count') countRef.value++

这里有一个设计层面的思考:为什么 Vue 3 不直接让解构自动保持响应式?因为 JavaScript 语言层面做不到——解构是把值拷贝出来,Proxy 拦截不到这个操作。所以必须通过toRefs显式声明"我要把这个属性变成独立的 ref 引用"。

在组合式函数里,toRefs几乎是标配。比如写一个useUser函数:

function useUser() { const user = reactive({ name: '张三', age: 25 }) // 返回时必须用 toRefs 包裹,否则外部解构后响应式丢失 return { ...toRefs(user) } } // 外部使用 const { name, age } = useUser()

还有一个相关概念:ref在模板中会自动解包,但reactive不会。如果你在模板里写了state.count而不是count,那解构与否其实都无所谓。我个人的习惯是:简单场景用 ref,复杂对象用 reactive,跨函数传递时统一转成 ref 形式,这样整个链路的数据流都是明确的。

4. defineModel:让 v-model 组件通信回归直觉

Vue 3.4 之后,父子组件之间做双向绑定可以告别props + emit('update:modelValue')这套啰嗦写活了。defineModel这个宏让 v-model 的使用体验和 Vue 2 的.sync修饰符一样直觉。

以前封装一个输入框组件,你需要这样写:

<!-- 子组件 --> <script setup> const props = defineProps({ modelValue: { type: String, default: '' } }) const emit = defineEmits(['update:modelValue']) function handleInput(e) { emit('update:modelValue', e.target.value) } </script> <template> <input :value="props.modelValue" @input="handleInput" /> </template>

有了defineModel之后:

<!-- 子组件 --> <script setup> const model = defineModel({ type: String, default: '' }) </script> <template> <input v-model="model" /> </template>

defineModel的本质是帮你做好了 props 声明和 emit 声明的联动:它默认声明一个modelValueprop,并在你修改返回值时自动 emitupdate:modelValue事件。父组件的用法保持不变:

<MyInput v-model="username" />

如果你需要多个 v-model,比如一个弹窗组件的"可见性"和"标题",defineModel也支持:

<!-- 子组件 --> <script setup> const visible = defineModel('visible', { type: Boolean, default: false }) const title = defineModel('title', { type: String, default: '' }) </script> <!-- 父组件 --> <MyDialog v-model:visible="dialogVisible" v-model:title="dialogTitle" />

这里有一个值得说清楚的点:defineModel对子组件内部的 ref 修改会同步到父组件,但如果你直接在子组件里对model赋一个新值,要确保走的是响应式更新而不是替换引用。比如:

// 推荐 model.value = '新值' // 不推荐:这种对象整体替换的场景要谨慎处理

defineModel在 Vue 3.4 之前是实验特性,3.4 正式稳定。如果你还在用 3.3 或更早版本,需要通过@vueuse/corevModel或者手动方式实现。如果是新项目,直接用 3.4+ 就好。

这个 API 最大的价值不只是少写了代码,而是让组件的 API 设计回归直觉。团队里新人看到v-model="xxx"就知道是双向绑定,不用再追到子组件里看 emit 了哪些事件名。

5. provide/inject 跨层级通信的适用范围与边界

组件通信的常规链路是 props 向下、emit 向上。但遇到"爷爷组件要给孙子组件传数据"这种跨越两三层以上的场景,props 链路会变得非常冗余——每一层都要声明一遍、传一遍,中间层组件被迫接收一堆自己根本用不到的数据。

provide/inject就是为这个场景设计的:

// 爷爷组件 import { provide, ref } from 'vue' const theme = ref('dark') provide('theme', theme)
// 孙子组件 import { inject } from 'vue' const theme = inject('theme') // 可以直接在模板里用 theme,也可以 theme.value

用起来很简单,但有两个关键细节必须掌握。

第一个是响应式传递。如果你传入的是一个普通字符串,它不会响应式更新:

// 错误示范:传了一个普通值 const theme = 'dark' provide('theme', theme) // 修改后,后代组件不会感知 function changeTheme() { theme = 'light' }

正确做法是传入 ref、reactive 或 computed:

const theme = ref('dark') provide('theme', theme) function changeTheme() { theme.value = 'light' }

因为 provide 传入普通值时,后代拿到的只是一个快照;传入 ref 时,后代通过.value读到的永远是最新的值。这一点和 props 的响应式原理不同,props 有 Vue 内部的响应式追踪,provide 没有——它完全依赖你传入的值本身是否具备响应式能力。

第二个是默认值处理。inject 可以接收默认值,但要区分两种写法:

// 写法一:只有一个默认值 const theme = inject('theme', 'dark') // 写法二:有默认值还可能有 undefined,需要区分 const theme = inject('theme', undefined) // 这种情况通常配合可选链使用

当父级没有提供对应 key 时,inject 返回默认值。如果你的组件可能被用在没有 provide 该 key 的父组件中,建议始终给一个安全的默认值,避免运行时产生undefined错误。

关于provide/inject和 Pinia 的取舍,我的经验是:provide/inject 适合局部共享,Pinia 适合全局共享。比如主题色、语言包、表单上下文这些只在一个子树内使用的状态,用 provide/inject 更轻量;但用户信息、权限标记、全局配置这种任何组件都可能用到的,放 Pinia 更合适。

6. keep-alive 与生命周期钩子:页面缓存的完整体验

SPA 应用里最常见的性能痛点之一:用户从列表页点进详情页,再返回列表页,列表重新请求接口、滚动位置丢失、表单输入清空。在移动端 H5 场景里这个问题尤其明显——用户可能只是切出去回个微信,再切回来页面状态就全没了。

keep-alive是 Vue 内置的缓存容器,包裹动态组件后,组件实例会被缓存而不是销毁:

<template> <keep-alive> <component :is="currentView" /> </keep-alive> </template>

但很多人只知道 keep-alive 缓存组件,不知道它改变了生命周期钩子的触发时机。被缓存的组件不会触发onUnmounted,而是触发onDeactivated;重新渲染时不会重新走onMounted,而是触发onActivated

import { onActivated, onDeactivated, onMounted } from 'vue' // 首次挂载时触发,缓存后再次进入不会触发 onMounted(() => { console.log('组件挂载') }) // 每次从缓存中重新进入时触发 onActivated(() => { console.log('组件被激活') // 在这里刷新列表数据、恢复滚动位置 }) // 每次切走时触发 onDeactivated(() => { console.log('组件被停用') // 在这里保存表单状态、移除定时器 })

有一个常见的坑:keep-alive 缓存的是组件实例,如果两个不同参数的同组件页面都放在 keep-alive 里,它们会共享同一个组件实例。比如一个详情页组件根据id参数渲染不同内容,从 A 详情切到 B 详情时,页面可能还在显示 A 的数据。通常的解决办法是给组件加:key="route.fullPath",让不同路径对应不同缓存项:

<keep-alive> <router-view :key="route.fullPath" /> </keep-alive>

keep-alive 还支持includeexclude属性,控制哪些组件需要缓存:

<keep-alive :include="['ListPage', 'DetailPage']" :max="10"> <router-view /> </keep-alive>

max属性限制最大缓存组件数,超出后会销毁最久未使用的实例。这个参数在大应用中很重要,否则缓存多了内存吃不消。

实际项目里我最常用 keep-alive 的场景是:Tab 切换页面保持状态、列表页返回保持滚动位置、表单页在浏览器后退时保留输入内容。每次用都要想清楚——哪些页面值得缓存,哪些页面必须每次重新拉数据(比如带权限校验的页面)。缓存是双刃剑,用好了体验提升明显,用不好就是"数据不刷新"的 bug 来源。

7. Teleport:弹窗组件的另一种挂载解法

弹窗、下拉框、全局提示——这些浮层组件在 CSS 上有一个共同的问题:它们会被父容器的overflow: hidden截断,或者在z-index层级上被盖住

比如这个场景:App.vue里有一个.content容器设置了overflow: auto,里面嵌套了一个弹窗组件。弹窗一旦超出.content的边界,就被裁掉了:

<!-- 布局结构 --> <div class="content" style="overflow: auto; height: 300px;"> <MyModal /> </div>

传统的解决方案是手动将弹窗 DOM 挂载到 body 上,比如各种原生 API 的appendChild。Vue 3 内置的Teleport让这件事变得非常简单:

<!-- MyModal.vue --> <script setup> import { ref } from 'vue' const visible = ref(false) </script> <template> <Teleport to="body"> <div v-if="visible" class="modal-overlay"> <div class="modal-content"> <slot /> <button @click="visible = false">关闭</button> </div> </div> </Teleport> </template> <style scoped> /* 这里的样式会照常生效 */ .modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); } </style>

Teleport做的事情是:把组件的内容渲染到to指定的 DOM 节点下,而不是它原本所在的组件树位置。但事件冒泡依然遵循组件树的逻辑,也就是说在 Teleport 内部的组件,它的emit事件还是能正常传给父组件。

这里有一个值得注意的细节:to目标必须存在于当前文档中,而且 Teleport 的挂载是在组件渲染时即时发生的。如果你在 SSR 场景使用 Teleport,需要确保目标元素已经渲染完毕,否则会报"target not found"错误。

Teleport还有一个disabled属性:

<Teleport to="body" :disabled="isEmbedded"> <MyModal /> </Teleport>

disabled为 true 时,Teleport 不会移动 DOM,组件会渲染在当前位置。这个能力在实现"同一个弹窗组件既能全屏展示、又能嵌入页面局部展示"的需求时非常有用。

实用场景远不止弹窗。全局加载条、消息通知、右键菜单、日期选择器的浮层面板——凡是会被容器裁剪或需要脱离组件树限制的,都可以用 Teleport。但也不要过度使用,简单的下拉框直接用绝对定位就行,没必要每次都往 body 上挂。

8. shallowRef:大数据渲染性能优化的第一刀

先理解refshallowRef的差别。

ref在实现上如果传入的是一个对象,内部会用reactive把这个对象整体变成深层响应式——对象里每一个嵌套属性都会被 Proxy 包裹,修改任何一个深层属性都会触发更新。

shallowRef只跟踪.value这一个层级的变化。也就是说,shallowRef.value指向的对象内部发生了什么,它不关心。

import { ref, shallowRef } from 'vue' // 深层响应式 const deepData = ref({ list: [{ name: '张三' }] }) deepData.value.list[0].name = '李四' // 会触发视图更新 // 浅层响应式 const shallowData = shallowRef({ list: [{ name: '张三' }] }) shallowData.value.list[0].name = '李四' // 不会触发视图更新 // 只有整体替换 .value 才会触发 shallowData.value = { list: [{ name: '王五' }] } // 会触发视图更新

很多人看到这里会问:那我为什么要用 shallowRef?

答案是性能。当一个 ref 包裹的对象非常庞大——比如一个包含几千条记录的表格数据、一个包含大量嵌套结构的地图配置、一个存储了图片 base64 的草稿对象——深层响应式的建立和跟踪成本非常高。每次修改一个深层属性,Vue 需要递归遍历、触发依赖比对,数据量越大越卡。

shallowRef 的适用场景非常明确:数据整体替换时更新,内部细节不需要响应式追踪

最典型的就是大数据列表:后端返回 500 条记录,前端只需要再对整体数据做一次替换(翻页、刷新),不需要修改其中某一条的属性。用 shallowRef 包裹列表数据,性能提升非常明显。

import { shallowRef } from 'vue' const tableData = shallowRef([]) function fetchData() { const res = await api.getList() // 整体替换,会触发更新 tableData.value = res.data.list }

如果确实有"整体替换之外也要触发更新"的需求,shallowRef提供了triggerRef

import { shallowRef, triggerRef } from 'vue' const state = shallowRef({ count: 0 }) // 修改内部属性后手动触发 state.value.count++ triggerRef(state) // 强制触发依赖更新

triggerRef的意义在于:shallowRef 设计上就是"手动控制触发时机",给你一个精确的触发扳机,想什么时候更新就什么时候更新。

还有一个容易混淆的概念是markRawmarkRaw是标记一个对象永远不会变成响应式,通常用于第三方实例、不可变配置等。shallowRef是"浅层响应式",markRaw是"完全非响应式",两者适用场景不同。我在优化大数据渲染时的经验顺序是:先看数据结构能不能shallowRef,再看哪些对象需要markRaw,最后才考虑虚拟滚动那些更重的方案。

9. onMounted 的时序陷阱:DOM、子组件与 nextTick

onMounted是前端开发里最常用的生命周期钩子,但它的时序逻辑有不少坑。

先明确一点:Vue 组件的 mount 过程是自底向上的。也就是说,子组件的onMounted先执行,父组件的onMounted后执行。这个顺序很多人不知道,导致在父组件的onMounted里访问子组件实例时,以为子组件已经完全准备好了,结果子组件内部的数据可能还没初始化完。

<script setup> import { ref, onMounted } from 'vue' import Child from './Child.vue' const childRef = ref(null) onMounted(() => { // 此时子组件的 onMounted 已经执行过了 console.log(childRef.value) // 父组件的 onMounted 里能拿到实例 }) </script> <template> <Child ref="childRef" /> </template>

第二个常见的坑:onMounted里访问 DOM 时,DOM 不一定处于最终状态。如果你的模板里有v-ifv-for、异步渲染的内容,可能在onMounted时 DOM 还没完全渲染完。这时候需要nextTick

import { onMounted, nextTick } from 'vue' onMounted(async () => { // 立即访问 DOM 的尺寸或位置可能不准 console.log(el.value.offsetHeight) // 等待 DOM 更新完成后再访问 await nextTick() console.log(el.value.offsetHeight) })

这里要掌握一个直觉判断:onMounted保证的是"组件已经挂载",但不保证"所有数据驱动的 DOM 更新已经完成"。如果 DOM 依赖的响应式数据在onMounted之前才被赋值(比如 setup 里异步请求),那onMounted时 DOM 可能还是初始状态。

还有一个被低估的场景:在onMounted里做定时器和事件监听。每次组件挂载都新增一个监听,如果不在卸载时移除,就会造成内存泄漏。Vue 3 的组合式 API 让清理变得更直观:

import { onMounted, onUnmounted } from 'vue' let timer = null onMounted(() => { timer = setInterval(() => { // 定时任务 }, 1000) }) onUnmounted(() => { clearInterval(timer) window.removeEventListener('resize', handleResize) })

有一种情况容易忽视:组件被 keep-alive 缓存时,onUnmounted不会触发,但定时器还在跑。被缓存的组件应该用onDeactivated暂停任务、onActivated恢复任务,而不是依赖onUnmounted。前面第 6 节讲过了,两个生命周期要配合着用。

10. 组合式函数 useXXX 的工程化设计:命名、清理与复用

组合式 API 最大的优势是逻辑复用。Vue 3 的组合式函数约定用use开头,比如useUserusePermissionuseCountdown。命名不是玄学,它是团队协作的信号——看到use开头,就知道这是一个可以在多个组件间复用的逻辑单元。

一个标准的组合式函数长这样:

// useCountdown.js import { ref, onUnmounted } from 'vue' export function useCountdown(initialSeconds = 60) { const seconds = ref(initialSeconds) let timer = null function start() { if (timer) return timer = setInterval(() => { if (seconds.value <= 0) { stop() return } seconds.value-- }, 1000) } function stop() { clearInterval(timer) timer = null } function reset() { stop() seconds.value = initialSeconds start() } // 组件卸载时自动清理定时器 onUnmounted(() => { stop() }) return { seconds, start, stop, reset } }

外部组件使用:

<script setup> import { useCountdown } from './useCountdown' const { seconds, start, stop, reset } = useCountdown(10) </script> <template> <button :disabled="seconds > 0" @click="start()"> {{ seconds > 0 ? `${seconds}s 后重试` : '获取验证码' }} </button> </template>

这个封装有几个关键的工程化细节:

第一,内部状态在函数内创建,还是模块级共享?这是最容易被忽略的问题。

// 模块级状态:所有调用 useCounter 的组件共享同一个 count const count = ref(0) export function useCounter() { return { count } } // 函数内状态:每个组件拥有独立的 count export function useCounter() { const count = ref(0) return { count } }

两种写法各有用途。模块级状态适合"全局唯一"的数据,比如用户登录态、站点配置;函数内状态适合"每个组件自己管一份"的数据,比如表单状态、列表筛选条件。多数情况下应该用函数内创建,除非明确需要全局共享。很多新手在这里踩坑——在某个组件里改了 useCounter 返回的 count,其他组件也跟着变了,排查半天才发现是模块级单例的"锅"。

第二,返回值要用 ref 类型,这样解构后依然保持响应式。如果你在组合式函数内部用reactive,在返回时将每个属性用toRefs转换后再返回。

第三,副作用清理应该封装在函数内部,而不是暴露给调用方。上面useCountdown里的onUnmounted在函数内部调用,对外完全透明——调用方不需要知道"用完要清定时器",只管解构返回值使用。

第四,组合式函数命名要按职责划分,不是越细越好。一个函数做一件事:useEventListener只负责事件绑定和清理,useRequest只负责接口请求状态管理,useDebounce只负责防抖。如果一个函数要做三件以上不相关的事,说明抽象粒度不对,该拆分了。

时间戳、防抖节流、键盘快捷键、滚动位置、鼠标坐标、剪贴板、媒体查询、权限校验——这些通用逻辑都可以抽成组合式函数。团队里维护一套内部业务封装的组合式函数库,比每个人在每个页面里复制粘贴同样逻辑要高效得多。

实际项目里,我的做法是:先从"重复出现三次以上"的逻辑开始抽,不要一上来就追求完美的抽象。抽函数之前先问自己三个问题——这段逻辑是否足够独立?是否会多个组件复用?参数和返回值是否清晰?三个问题都答是的再动手。代码量增长到一定规模后,组合式函数的边界设计能力直接决定项目的可维护性。


最后分享一点我个人的体会。在 Vue 3 项目里写代码,真正让你和普通开发者拉开差距的,不是背了多少 API 名字,而是能不能把每个 API 用在最合适的场景里。computedwatch看似相似但职责完全不同;shallowRef在 90% 的场景用不上,但用到一次就能解决一个卡顿问题;defineModel只是一个宏,但它让组件通信的代码量减少了一半。Vue 3 的坑不多,但每个坑都藏在"好像都能用"的 API 组合里。写代码时多问一句"为什么这么写",比任何面试八股都有意义。

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

RustFox:10MB、启动<1秒的极简API调试工具原理与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 16:08:17

NRBO-SVM多变量时序预测在Matlab中的实现与优化

1. 项目背景与核心价值 在工业预测和科研分析领域&#xff0c;多变量时序预测一直是个硬骨头。传统单一模型往往顾此失彼——要么抓不住长期趋势&#xff0c;要么忽略短期波动&#xff0c;更别提超参数调优这个老大难问题。最近在Matlab圈子里火起来的NRBO-SVM组合拳&#xff0…

作者头像 李华
网站建设 2026/9/14 16:07:30

ClickHouse v23.10.6.60-stable 更新详解:22 项 Bug 修复与源码定位

ClickHouse v23.10.6.60-stable 更新详解&#xff1a;22 项 Bug 修复与源码定位 【免费下载链接】ClickHouse ClickHouse is a real-time analytics database management system 项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse 本文基于当前仓库中的版本…

作者头像 李华
网站建设 2026/9/14 16:04:48

全固态激光雷达如何守护铁路安全:异物侵限监测实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 16:04:42

零代码UI自动化:基于浏览器原生能力的回归测试新范式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华