vue3学习笔记

vue3学习笔记
晨梦如果发现FCP比较大,有什么优化思路
观察: Chrome DevTools → Performance 面板,点 “刷新记录 (⟳)” 按钮进行一次录制FCP是衡量页面首次绘制内容的时间指标(图片、文字等),直接影响用户‘页面是否开始加载’的感知. 如果FCP较大,意味着首屏加载慢,用户可能觉得网站没反应.
资源体积大、加载慢
JS/CSS 图片 打包未压缩、未按需引入
解决:
在 vite.config.ts 中,Vite 配置在 build 模式下使用 esbuild 压缩 JS、cssCodeSplit 压缩 CSS
在webpack的生产环境 plugins插件中用MiniCssExtractPlugin 提取 css 的公共部分, 有效利用缓存 optimization优化项里压缩 代码分割等配置
图片: 尽量用webp格式
首页组件、库太多,一次性加载太多内容
vite:
里面配置 optimizeDeps 预加载项目必须组件
在plugins里面配置Components 自动按需引入
webpack:
vue2中 插件里配置 然后组件
import { Button, Select } from 'element-ui'
vue3 中 直接用unplugin-auto-import插件自动按需引入
首页中部分组件懒加载 异步加载组件
比如某个组件3D图表,PDF预览等只有在按钮点击后展示,首次不加载
import {defineAsyncComponent} from 'vue'
const AsyncComp = defineAsyncComponent(()=> {
import('@/components/HeavyComponent.vue')
})
文件解析过程比较长
滚动到视图再加载
路由懒加载 图片懒加载:使用 loading=”lazy”
首屏加载 DOM 节点太多
减少首屏DOM数量 分页或者懒加载 虚拟滚动列表
webpack 中 优化 配 代码分割 拆包 比如分成 node_modules 项目公共模块 等拆分出来
vite中 rollupOptions 里面 手动拆包
TTFB(首字节时间)过长
衡量的是浏览器发出请求后,接收到服务器返回第一个字节的时间
目的: 提升服务器响应速度,网络传输效率
后端渲染慢、数据库慢
niginx 使用HTTP缓存策略 ,配置强缓存 协商缓存, vite构建后的文件自带hash所以可以放心使用(强: 浏览器先检查本地缓存是否可用,可以就用 不发请求 Cache-Control:max-age=3600 协商:当强缓存失效,会发请求,并携带资源的标识信息 时间戳或者hash 没变 304继续缓存 变了 200 新资源 )
让后端采取redis本地缓存
使用http/2 开启多个tcp 避免丢包机制
静态资源走 CDN
预加载 在浏览器空闲时间做预加载requestIdleCallback 避免阻塞组主线程
script setup到底做了什么
在 Vue 3 中, setup 是对 Vue 组合式 API(Composition API)的一个简化语法糖。当使用 setup() 方式时,可以通过 setup(props, { expose }) 的 expose() 方法 来暴露实例成员
在 script setup 语法中,Vue 提供了一个编译时宏 defineExpose()(仅在编译时生效,不会在运行时生成额外代码),用来暴露实例成员,不会影响性能
对路由的理解
创建路由: 使用 createRouter 和 createWebHistory 或 createWebHashHistory 创建路由实例。
const router = createRouter({
history: createWebHistory(),
routes: staticRoutes,
scrollBehavior() {
return {top: 0, left: 0}
},
})
路由模式:
- history 模式(createWebHistory):基于 HTML5 History API,不带 #。有利于SEO优化,需要服务器支持,刷新可能会404 需要重定向
- hash 模式(createWebHashHistory):基于 URL 哈希 (#),兼容性好,但 URL 不够美观,且爬虫爬不到#后的内容。由于hash不发送请求,所以刷新不会404
动态路由:使用 :id 或 :name 定义动态路径,例如 /user/:id。
路由懒加载:通过 import() 动态引入组件,减少首屏加载时间。
导航守卫:
- 全局守卫(beforeEach、afterEach):控制全局路由访问权限。
- 路由独享守卫(beforeEnter):用于单个路由。
- 组件内守卫(beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave):控制组件级别的导航逻辑。
**编程式导航:**使用 router.push()、router.replace() 进行跳转。
路由传参:
- 路径参数(params):如 /user/:id,使用 $route.params 获取。
- 查询参数(query):如 /user?id=1,使用 $route.query 获取。
css动画@keyframes和过渡的区别
定义方式
- @keyframes(动画):需要通过 @keyframes 规则定义动画的关键帧,并配合 animation 属性使用。定义多个关键帧(0% ~ 100%),从而实现更复杂的动画效果,如循环、暂停、反向播放等。
- transition(过渡):直接在 CSS 规则中定义,只能对某些属性在两个状态之间进行平滑过渡。
触发方式
- @keyframes:动画可以自动执行(animation 设置 animation-play-state: running)或由 JavaScript 控制。
- transition:需要某种状态变化(如 hover、focus 或 JavaScript 代码修改样式)来触发。
示例
@keyframes 实现循环动画
@keyframes move {
0% { transform: translateX(0); }
50% { transform: translateX(100px); }
100% { transform: translateX(0); }
}
.box {
width: 100px;
height: 100px;
background-color: red;
animation: move 2s infinite;
}
transition 实现鼠标悬停变化
.box {
width: 100px;
height: 100px;
background-color: red;
transition: background-color 0.5s ease, transform 0.5s;
}
.box:hover {
background-color: blue;
transform: scale(1.2);
}
总结
- transition 更适合简单的状态切换,如 hover 变色、大小变化等。
- @keyframes 更适合复杂的动画,如循环播放、多个关键帧控制等。
- 性能上,transition 一般比 @keyframes 更优,但两者都可以利用 GPU 加速(如 transform)。
如果动画是由用户交互(如 hover、click)触发,并且只需要在两个状态之间平滑过渡,用 transition。
对vue3生命周期的理解
创建前/后 beforeCreate / created → 由 setup() 代替执行初始化逻辑。
作用:1.初始化响应式数据(ref、reactive) 2.注册生命周期钩子(如 onMounted) 3.返回组件中使用的数据与方法 4.没有访问 this,因为虚拟dom尚未生成,组件实例还没创建。
挂载前/后 beforeMount → onBeforeMount() DOM 挂载前调用。 虚拟dom正在构建VNode树,但尚未渲染到真实DOM中
mounted → onMounted() 组件挂载完成,也就是在DOM上进行渲染完成,DOM 可访问。响应式的副作用开始追踪依赖
作用: 1.发送请求 2.操作 DOM(比如获取 canvas 元素) 3.初始化图表、视频等依赖真实 DOM 的内容
更新前/后 响应式数据发生变化,生成新树,新旧两个虚拟dom树通过 diff算法比较生成最小化的dom操作就是patch, beforeUpdate → onBeforeUpdate()
updated → onUpdated() patch结果同步到真实DOM后调用,DOM 更新完成
卸载前/后 组件将要被卸载,虚拟DOM将要被销毁。对应的VNode也被标记为失效 beforeUnmount → onBeforeUnmount() 用途:清理定时器、取消订阅、解绑事件等
unmounted → onUnmounted()组件卸载完成。组件的副作用与VNode都已经解除绑定响应式依赖被释放 用途:通常用于清理收尾逻辑的确认。
onActivated() 和 onDeactivated()作用:用于
onErrorCaptured() 作用:捕获子组件中的错误,类似 try-catch。
onActivated() 和 onDeactivated()的使用场景:
当你有多个子组件切换显示,并使用
<template>
<keep-alive>
<ComponentA v-if="current === 'A'" />
<ComponentB v-if="current === 'B'" />
</keep-alive>
</template>
在这种结构下,ComponentA 和 ComponentB 会被缓存,切换时不会重新销毁,而是停用(deactivated)和激活(activated)。
SPA与MPA的区别
| 特性 | SPA | MPA |
|---|---|---|
| 页面数量 | 一个页面 | 多个 HTML 页面 |
| 路由控制 | 前端控制(如 Vue Router) | 后端控制(如 Spring Boot、PHP) |
| 页面跳转 | 无刷新、局部渲染 | 整页刷新、跳转 |
| 首屏加载速度 | 较慢(依赖 JS 渲染) | 较快(HTML 后端直接渲染) |
| 交互体验 | 更流畅,接近原生应用 | 跳转明显、体验中等 |
| SEO(搜索引擎优化) | 不友好(需 SSR 或预渲染优化) | 友好,直接输出 HTML |
| 状态管理 | 前端统一管理(如 Pinia、Vuex) | 页面间状态不共享,需后端或缓存支持 |
| 开发难度 | 高(需构建、打包、路由、权限等) | 相对简单,传统开发流程 |
三栏布局的实现方式(圣杯模式)
三栏布局包括一个固定宽度的左侧栏、一个固定宽度的右侧栏,以及一个自适应宽度的主要内容区域
- flex布局 整体盒子display:flex; 左右固定宽度例如width:200px;中间flex:1
- 浮动布局 整体盒子width:100%; 左盒子固定宽度例如200px,float:left;右盒子固定宽度例如200px,float:right;中间盒子margin-left和margin-right分别200px
- 绝对定位 整体盒子position:relative;左右盒子宽度固定200px;position:absolute; top:0;左盒子left:0;右盒子right:0;最后中间盒子margin: 0 200ox;
画一条0.5px的线
.line {
width: 100%;
height: 1px;
background-color: #ccc;
transform: scaleY(0.5);
transform-origin: top; /* 缩放的基准点是“上边缘”,即从上向下缩放 */
margin: 0; /*把上下左右外边距都清除*/
}
ref和reactive 的区别
- 数据类型不同
ref用于包装JS的任意对象(基本数据类型+对象),而reactive用于包装对象和数组等复杂类型的数据。 - 使用方式不同:
如果将一个对象赋值给 ref,那么这个对象将通过reactive转为具有深层次响应式的对象
ref需通过.value访问 reactive可以直接访问该对象的属性或方法
原理:
如果将一个对象赋值给 ref,那么这个对象将通过reactive转为具有深层次响应式的对象
而reactive响应式的原理是建了一个被Proxy代理的对象,Proxy里面代理了各种操作,在读取的时候触发track函数,在写入的时候触发trigger函数。
ref的本质就是实例化了RefImpl类得到了一个对象,访问这个对象的value属性时触发track,设置这个对象的value属性时触发trigger
使用原则:
若需要一个响应式对象,且层级较深,推荐使用 reactive
.ts 与 .d.ts 的区别
.ts 文件: 编写实际的 代码实现
.d.ts : 提供 类型声明,不包含具体实现
什么是泛型
泛型是一种在定义函数、接口或类时不预先指定具体类型,而在使用时再指定类型的特性
泛型的核心思想是参数化类型
增强代码复用性:一份代码适用于多种类型。
提高类型安全性:比 any 更安全,类型错误可在编译阶段被发现。
泛型接口
interface Result<T> {
success: boolean;
data: T;
}
当函数或组件/类的类型不确定、但希望保持类型一致性时,非常适合使用泛型。例如:
一个组件可以接收任意数据类型,但需要保留原始类型
一个工具函数既可以处理数组、也可以处理对象
在封装通用接口响应时,不知道 data 是什么类型
联合类型和交叉类型
- 联合类型 A | B:变量可以是多种类型中的一种,灵活但不具备所有属性
- 交叉类型 A & B:变量必须同时符合多个类型,具有所有属性的并集。
ts的数据类型
1. 基础类型
number string boolean null undefined symbol(唯一值标识符) bigint(表示最大整数)2. 引用类型
object 任意类型Array<T>数组,元素类型为 T Function 函数类型 Date 日期对象3. 特殊类型
any 任意类型 unknown 安全的any,使用前必须先断言类型或做类型判断 void 没有返回值的函数 never 永远不会返回(比如抛出异常或死循环)4. 类型组合与别名
联合类型 交叉类型 类型断言 any 任意类型 unknown 安全的any,使用前必须先断言类型或做类型判断 void 没有返回值的函数 never 永远不会返回(比如抛出异常或死循环插件和组件的区别
组件: 封装 UI 和交互 的独立模块,局部或页面级别,用于渲染视图,import 后直接使用或局部注册,通常包含 template + script + style,常用于 复用 UI 模块,如按钮、弹窗、表单项等。
插件:用于为整个 Vue 应用添加全局功能, 全局注册,影响整个 Vue 应用,app.use(plugin) 注册,纯 JavaScript,提供 install(app) 方法, 封装功能模块,如 axios 封装、权限控制、国际化等
插件用法:
- 插件结构: 必须提供 install 方法
// plugins/myLogPlugin.ts
export default {
install(app) {
app.config.globalProperties.$log = (msg: string) => {
console.log(`[LOG]: ${msg}`);
};
}
}
- 注册插件
// main.ts import { createApp } from 'vue' import App from './App.vue' import myLogPlugin from './plugins/myLogPlugin' const app = createApp(App) app.use(myLogPlugin) app.mount('#app') - 在组件中使用插件功能
<script setup> // @ts-ignore $log('这是插件中的日志功能') // 使用全局属性 </script>MES(制造执行系统)
车间级执行控制系统:生产现场(人、机、料、法、环)机台管理 工艺管理 生产管理 品质检验
什么是虚拟dom
是通过js对象来模拟真实的DOM结构,当组件的状态或数据发生变化的时候,vue会先在虚拟DOM中变更,而不是直接操作真实DOM,通过diff算法比较比较新旧虚拟DOM树的差异 来生成patch(最小化生成dom操作)最后将这组更新批量用到真实DOM上。
作用:用这种方式可以减少对DOM的操作次数,从而提高页面的渲染效率。
vue双向绑定原理
Vue 的双向绑定是指视图和数据之间的自动同步。当数据变化时视图自动更新,视图变化时数据也会自动同步。通常通过 v-model 实现。
1. vue2中
v-model 主要用于双向数据绑定(收集表单数据),它给元素绑定时,不同元素做法不一样:
<input v-model="name" /> 等价于
<input :value="name" @input="name = $event.target.value" />
v-model双向数据绑定本质就是对数据的读和写,读都是使用单向数据绑定去做,而写的话都是通过事件去处理
- 如果是文本类型元素(
<input> 和 <textarea>):绑定 value 属性去读取数据 和 input 事件去写数据; - 如果是单选或多选元素(
<input type="checkbox">和<input type="radio">):绑定 checked 属性和 change 事件; - 如果是下拉列表元素(
<select>):绑定 value 属性和 change 事件; - 如果不是上述这些元素(比如组件),会按照文本类型元素处理。
总结:
v-model 在表单元素上,本质上是 :value 和 @input 的语法糖。
使用 Object.defineProperty() 实现数据劫持。
更新数据时会触发视图更新,用户输入时触发事件同步数据。
2. vue3中:
父组件
<MyInput v-model = "msg">
实际Vue帮转成
<MyInput :modelValue="msg" @update:modelValue="msg = $event" />
绑定值: 实际上给子组件的是modelValue属性
响应:监听子组件发出的 update:modelValue 事件,并赋值给 msg
子组件
我们需要通过defineProps来接收modelValue
然后通过defineEmits发出defineEmits([‘update:modelValue’])事件来实现数据的更新
最后,在模板中,我们把modelValue绑定到input的value上,再通过监听@input事件,把新值发出去
这样一来,父组件的数据就根据子组件输入实时变化,实现了双向数据绑定
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const update = (val) => {
emit('update:modelValue', val)
}
</script>
<template>
<input :value="modelValue" @input="update($event.target.value)" />
</template>
3.v-model 的多参数
Vue 3 支持多个 v-model:
父组件
<Child v-model:title="title" v-model:content="content" />
等价于:
<Child
:title="title"
@update:title="val => title = val"
:content="content"
@update:content="val => content = val"
/>
子组件
const props = defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])
Set和Map
Set: 值的集合 是一种新的数据结构,类似数组(只存值),用于存储唯一值的集合
Map: 键值对集合 是一种新的数据结构,类似对象(键值对),用于存储键值对的数据结构
WeakSet: 对象的集合 存储内容只能是对象,不能是原始类型,适合用于存储一组临时对象、不追踪生命周期
WeakMap: 对象键到任意值的映射 存储内容键必须是对象,值可以是任意类型,适合用于给对象附加元信息、不阻止垃圾回收
这两个都是弱引用,被引用对象若无其他引用会被回收,值都不可遍历
在项目中我曾遇到一个需求:需要对用户上传的设备 ID 列表进行去重处理,并且在后续根据设备 ID 快速查找设备详情。
这就涉及两个问题:
一是如何高效去重,
二是如何用设备 ID 映射到设备信息上。
在业界的常规做法中,对于去重,大家普遍使用的是 Set 数据结构,因为它能自动去除重复项;
而对于键值映射,通常用 Map 数据结构,相比传统对象,它支持任意类型作为 key,性能也更优。
我在项目中最终的解决方案是:
使用 Set 对上传的设备 ID 进行去重,确保不重复发请求;
然后用 Map 来存储设备 ID 到设备信息的映射,这样后续在 UI 上展示或查找时,只需通过 map.get(id) 即可快速访问,无需遍历数组或过滤。
落地效果上,这种方式不仅代码可读性更强,也显著提升了查找效率,设备渲染页面的响应时间缩短了约 30%。同时,由于结构清晰,也方便了后期维护和扩展。
vite和webPack的区别
- 底层语言不同
vite是用go语言编写的, go语言是纳秒级别的, 而webpack的js是以毫秒计数的, 所以vite比webpack打包快. - 启动方式不同
webPack: 分析依赖 (通过递归分析入口文件,构建完整的模块依赖图,将所有资源(JS/CSS/图片等)转换为模块) –> 编译打包 –> 交给本地服务器进行渲染, 模块越多, 热更新越慢
vite: 启动服务器 –> 请求模块时, 按需动态编译显示, 它在启动的时候不需要打包, 所以不用分析模块与模块之间的依赖关系
缺点比较:
- vite的加载器和插件没有webPack丰富
- vite在打包到生产环境的时候, esbuild构建对于css和代码分割不够友好, 所有vite的优势是体现在开发阶段的
- vite首屏性能没有webpack好
- webpack:浏览器发送请求, 服务端把已经打包构建好的首屏内容发给浏览器, 整个过程不存在性能问题
- vite: 他有个unbundle机制, 首屏期间还要做额外的事, 比如: 不对源文件做合并捆绑的操作, 导致大量http请求. dev server运行期间要对源文件做resolve、load、transform等操作预构建,二次构建操作也会阻塞首屏请求, 直到预构建完成为止
讲一下tcp的队头阻塞
队头阻塞:TCP 是一种丢包重传机制-丢了就得重传,不能跳过。、流量控制与拥塞控制-网络拥堵会导致延迟、重传,从而引发更严重的阻塞。、按序传递-到的数据必须按照顺序传递给上层应用。的协议
如果一个数据包丢失或延迟,后面的所有数据都要等它重新传输回来,才能被上层应用读取。这就是 TCP 的“队头阻塞”问题 —— 前面的数据没到,后面的也用不了。
假设你坐地铁,一节车厢的人必须一个个下车,前面的人卡住了,后面的人也下不去。
在 TCP 中也是这样:你从浏览器发送多个请求;第一个数据包(如请求 A)在传输中丢了;TCP 会等这个包重传回来;后面请求 B、C 的数据虽然已经到达,但不能交给应用层处理;浏览器就表现为卡顿或延迟。
在实际开发中的影响:
在 HTTP/1.1 中,多个请求共用一个 TCP 连接,队头阻塞会导致整个网页加载变慢。为此,HTTP/2 浏览器会开启多个 TCP 连接并发请求,但这又带来 TCP 握手和资源浪费的问题.HTTP/3采用基于 UDP 的 QUIC 协议,彻底解决 TCP 队头阻塞问题.
vue2和vue3的区别
响应式不同:
组合式API
vue2 主要通过选项式API(如 data, methods, computed 等)进行组件的逻辑组织。
vue3 引入了组合式API(如 ref, reactive, computed, watch), 这为逻辑复用和代码组织提供了更灵活的方式
性能相关:
vue3 在性能方面有显著提升,它包括更小的打包大小,更快的虚拟DOM重写、更高效的组件初始化
vue2 相比之下在性能方面相对较慢, 尤其是在处理大型应用和复杂组件
typeScript支持
vue3 可以支持TypeScript编写, vue2 对TypeScript支持是有限
vue3允许多个节点, 这使得组件模版可以有多个并列的根元素。vue2 要求每个组件必须有一个单独的根节点
vue2打包体积过大,而vue3用了树摇机制,会除去未被使用的代码部分,打包文件小更快。
静态元素的提升
vue2中, 模版中的所有元素在每次渲染时都会被创建新的虚拟节点,包括静态元素
vue3中, 引入了静态元素提升的概念, 在编译模版时, vue3会检测出静态内容将其提升, 也就是内容只在初次渲染的时候创建一次. 后续过程, 静态内容就会被复用, 提升性能
虚拟dom渲染方式
vue2 在更新组件, 会进行全面的dom比较, 这可能会导致性能开销
vue3 引入了patch, 它在编译的时候标记虚拟节点的动态部分,组件更新时,只需要关注这些被标记的部分,而不是整个组件树,从而提升性能
生命周期变化 见以上
组件传值的方式都有哪些
- props(父 → 子): 父组件通过 props 向子组件传递数据。
<!-- Parent.vue -->
<Child :msg="parentMsg" />
<!-- Child.vue -->
<template>{{ msg }}</template>
<script setup>
defineProps({ msg: String })
</script>
- emit(子 → 父): 思想:自定义事件,子组件通过 $emit 触发事件,向父组件传递数据。
vue3 中, 子往父进行传值, 需要使用 defineEmits 宏方法 他接收一个数组作为参数,参数中就是要发射的自定义事件
defineEmits 具有返回值,返回的是一个方法,然后调用返回的方法,给方法传入自定义的事件即可
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['updateMsg']) // 接收一个数组作为参数,参数中就是要发射的自定义事件
const sendMsg = () => emit('updateMsg', 'new data')
</script>
<!-- Parent.vue -->
<Child @updateMsg="handleMsg" />
兄弟组件通信 mitt: 安装并使用 mitt 作为事件总线进行兄弟组件通信。
npm install mitt
新建lib(第三方库)文件夹创建 mitt.ts
代码如下:// 导入 mitt import mitt from 'mitt' // 对 mitt 进行实例化 const emitter = mitt() // 将实例暴露出去 export default emitterChild组件
<button @click="sendData">传值给兄弟组件</button> <script lang = "ts" setup> import emitter from '@/lib/mitt' const sendData = () => { // 通过 emit 发射事件 emitter.emit('myevent', 10) } </script>Son组件
<script lang = "ts" setup> import emitter from '@/lib/mitt' import { onMounted } from 'vue' onMounted(() => { // 通过 on 接收事件 回调里能拿到传过来的值 emitter.on('myevent', (val) => { console.log(val) }) }) </script>跨层级组件通信 provide / inject: 祖先组件通过 provide 提供数据,后代组件通过 inject 注入。
App.vue<script setup> // 祖先组件使用 provide 是一个方法 接收两个参数 第一个是需要传递的数据属性 第二个参数 具体的值 import { provide } from 'vue' provide('theme', 'dark') <script/>Child.vue
<script setup> import { inject } from 'vue' const theme = inject('theme') </script>状态管理 Pinia
npm install pinia// store.ts import { defineStore } from 'pinia' export const useMainStore = defineStore('main', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })ref / v-model
ref 引用子组件并调用其方法或属性:<!-- Parent.vue --> <Child ref="childRef" /> <script setup> import { ref, onMounted } from 'vue' const childRef = ref() onMounted(() => { childRef.value.someMethod() }) </script>v-model 双向绑定(支持多个字段)
Vue 3 引入了更灵活的 v-model 机制,支持多个 v-model 绑定,支持自定义绑定属性名(modelValue),并将事件名称更改为 update:modelValue,使得组件的双向绑定更加灵活和语义化。【默认绑定modelValue 属性和 update:modelValue 事件】
// Parent.vue
<Child
v-model:title="title"
v-model:content="content"
/>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const title = ref('初始标题')
const content = ref('初始内容')
</script>
// Child.vue
<template>
<input
:value="title"
@input="e => emit('update:title', e.target.value)"
placeholder="输入标题"
/>
<textarea
:value="content"
@input="e => emit('update:content', e.target.value)"
placeholder="输入内容"
/>
</template>
<script setup>
const props = defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])
</script>
解释:
在 Parent.vue 中,我使用了 v-model:title=”title” 和 v-model:content=”content” 将两个响应式数据分别传递给子组件的 title 和 content prop。Vue 会自动将这两个 v-model 语法糖转换为:
:title="title"
@update:title="val => title = val"
:content="content"
@update:content="val => content = val"
而在 Child.vue 中,我通过 defineProps([‘title’, ‘content’]) 接收这两个 prop,并在用户输入时,通过 emit(‘update:title’, xxx) 和 emit(‘update:content’, xxx) 主动触发事件,将最新的值传回父组件,实现了双向绑定。
watch和computed有什么区别
computed:
有缓存,依赖于vue的响应式系统来进行数据追踪,依赖变化时才重新计算 性能更高效,仅在访问时才执行计算函数
使用场景: 需要展示的衍生状态 ,复杂模板绑定需要计算的,避免重复逻辑、表单数据转换(比如根据选中的地区自动拼接完整地址)
watch:
没有缓存,只要监听值变化就会触发, watch是惰性执行,也就是只有监听的值发生变化的时候才会执行(除非配置immediate参数,立即执行,以及深层次监听)
watch需要传递监听的对象(数据源可以是响应式引用(ref)、计算属性(computed)或者是一个返回响应式值的函数)
使用场景: 处理异步操作或副作用 如 根据参数变化调用 API、搜索功能,根据用户输入发请求通常配合防抖,监听路由参数变化并执行数据更新
watchEffect:
vue 3 新增,watchEffect在组件初始化时会立即执行一次,用来收集依赖。(像watch + immediate)
不需要指定监听对象,它会自动追踪函数中用到的所有响应式数据
适用于: 想立即执行某些响应操作
🌟 场景:根据城市和天气类型查询天气信息
import { ref, watchEffect } from 'vue';
const city = ref('北京');
const weatherType = ref('晴天');
const weatherData = ref(null);
// 传统 watch 写法
watch([city, weatherType], ([newCity, newType]) => {
fetchWeather(newCity, newType).then(res => {
weatherData.value = res;
});
});
// watchEffect 更简洁写法
// 自动监听 city 和 weatherType,无需手动指定
watchEffect(async () => {
if (!city.value || !weatherType.value) return;
const res = await fetchWeather(city.value, weatherType.value);
weatherData.value = res;
});
总结
在写业务逻辑的时候遇到:
要展示的值是根据其他值计算出来的 → 用 computed
要根据某个值变化执行一个逻辑 → 用 watch
只是写个副作用逻辑,能自动监听响应式就行 → 用 watchEffect
你怎么封装一个组件的
功能性组件封装
当我封装一个功能性组件时,比如一个弹窗、表格、分页、表单项等,我会按照下面几个步骤来进行
1. 分析输入输出
明确组件需要哪些 props(输入参数),是否需要支持 v-model 双向绑定
考虑组件的状态变化是否需要通知外部,比如通过 emit 触发 update:modelValue、change 等事件。
是否需要支持插槽,比如操作按钮、内容扩展等
2. 使用<script setup>模式实现组件逻辑
我会使用 Vue3 的 <script setup> 模式,使用 defineProps 接收参数,defineEmits 触发事件,封装好内部状态管理(如用 ref/watch 等)。如果需要暴露方法给外部调用,我会用 defineExpose。展等。
UI 组件封装
例如: 二次封装 elementUI 组件
增强组件能力,但不破坏原有功能,保持所有原始 props 和事件透传。
保证 elementUI 原有属性可以被使用,增强而不破坏
$attrs: 一个对象,包含 父组件传过来但子组件没声明的所有属性
$listeners: Vue2 特有的对象,包含 父组件绑定的所有事件监听函数
通过 v-bind=”$attrs”() 和 v-on=”$listeners” / defineEmits()(Vue3)保持原始属性/事件向下透传
<el-input v-bind="$attrs" v-on="$listeners" />
在 Vue 3 中,$attrs 仍然可用,但 $listeners 被合并进 $attrs,事件监听也可以一起透传
异步加载组件的做法
vue3官方提供的API defineAsyncComponent
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('@/components/HeavyComponent.vue')
)
这样可以 懒加载组件,按需加载资源,从而优化性能、减少首屏体积、提升用户体验。
所以当某个组件不在首次渲染时就需要展示,或者体积大、加载慢、使用频率低时,就适合用 defineAsyncComponent 进行懒加载。
场景:某个组件如 3D 图、图表、PDF 预览、富文本编辑器等,只在点击按钮后展示,首次不加载。
<template>
<el-button @click="show = true">打开编辑器</el-button>
<Editor v-if="show" />
</template>
<script setup>
import { defineAsyncComponent, ref } from 'vue'
const show = ref(false)
// 异步加载富文本编辑器组件
const Editor = defineAsyncComponent(() => import('@/components/RichEditor.vue'))
</script>
持续更新中…






