1. Vue.js 框架概述
Vue.js 是一款渐进式 JavaScript 框架,由前 Google 工程师尤雨溪于2014年首次发布。经过近十年的发展,Vue 已经成为全球最受欢迎的前端框架之一,在 GitHub 上拥有超过20万星标。Vue 的核心设计理念是"渐进式"——开发者可以根据项目需求逐步采用框架的不同功能,从简单的视图层渲染到复杂的单页应用开发。
1.1 Vue 的核心特性
Vue 之所以能在众多前端框架中脱颖而出,主要得益于以下几个核心特性:
-
响应式数据绑定:Vue 的响应式系统会自动追踪数据变化并更新 DOM,开发者无需手动操作 DOM 元素。这种机制大大简化了前端开发中最繁琐的数据-视图同步工作。
-
组件化开发:Vue 提供了强大的组件系统,允许开发者将界面拆分为独立、可复用的组件。每个组件都有自己的模板、逻辑和样式,可以像搭积木一样构建复杂的用户界面。
-
虚拟 DOM:Vue 使用虚拟 DOM 技术来提高渲染性能。当数据变化时,Vue 会先在内存中计算最小化的 DOM 操作,然后再应用到真实 DOM 上。
-
丰富的生态系统:Vue 拥有完善的周边工具链,包括路由(vue-router)、状态管理(Pinia/Vuex)、构建工具(Vite/Vue CLI)等,可以满足各种规模项目的开发需求。
1.2 Vue 2 与 Vue 3 的主要区别
Vue 3 于2020年9月正式发布,带来了多项重大改进:
-
性能提升:Vue 3 的打包体积更小(约10KB gzipped),初始渲染速度快55%,更新速度快133%,内存占用减少54%。
-
Composition API:引入了全新的组合式 API,提供了更好的逻辑复用和代码组织方式。
-
更好的 TypeScript 支持:Vue 3 完全使用 TypeScript 重写,提供了更好的类型推断和开发体验。
-
新的响应式系统:使用 Proxy 替代 Object.defineProperty,解决了 Vue 2 中数组和对象属性添加/删除的响应式问题。
-
新特性:引入了 Fragments(多根节点组件)、Teleport(传送门)、Suspense(异步组件)等新功能。
2. Vue 基础语法详解
2.1 创建 Vue 应用
Vue 2 应用创建
在 Vue 2 中,我们通过 new Vue() 构造函数来创建应用实例:
javascript复制// main.js
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App)
})
Vue 3 应用创建
Vue 3 引入了 createApp 工厂函数来创建应用实例:
javascript复制// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
这种改变带来了几个优势:
- 每个应用实例相互独立,不会共享全局配置
- 更清晰的API边界
- 更好的TypeScript类型支持
2.2 模板语法
Vue 使用基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定到底层组件实例的数据。所有 Vue 模板都是合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
文本插值
最基本的数据绑定形式是使用"Mustache"语法(双大括号)的文本插值:
html复制<span>Message: {{ msg }}</span>
双大括号中的内容会被替换为对应组件实例的 msg 属性值,当 msg 改变时,插值处的内容也会更新。
原始 HTML
双大括号会将数据解释为纯文本,如果要输出真正的 HTML,需要使用 v-html 指令:
html复制<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
注意:在网站上动态渲染任意 HTML 存在安全风险,容易导致 XSS 攻击。只对可信内容使用
v-html,永远不要将用户提供的内容作为 HTML 插值。
属性绑定
Mustache 语法不能在 HTML 属性中使用,应该使用 v-bind 指令:
html复制<div v-bind:id="dynamicId"></div>
v-bind 可以简写为 ::
html复制<div :id="dynamicId"></div>
JavaScript 表达式
Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:
html复制{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="'list-' + id"></div>
这些表达式会在当前组件实例的数据作用域下作为 JavaScript 被解析。有个限制是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效:
html复制<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}
<!-- 流程控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}
2.3 指令
指令是带有 v- 前缀的特殊属性。指令属性的值预期是单个 JavaScript 表达式(除了 v-for 和 v-on)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
条件渲染
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
html复制<h1 v-if="awesome">Vue is awesome!</h1>
也可以用 v-else 添加一个"else 块":
html复制<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
v-else-if 可以连续使用:
html复制<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
另一个用于条件展示元素的指令是 v-show。用法大致一样:
html复制<h1 v-show="ok">Hello!</h1>
不同之处在于:
v-if是"真正"的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-if也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。v-show就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换(display 属性)。
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
列表渲染
v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。
html复制<ul>
<li v-for="item in items" :key="item.id">
{{ item.message }}
</li>
</ul>
在 v-for 块中,我们可以访问所有父作用域的属性。v-for 还支持一个可选的第二个参数,即当前项的索引。
html复制<ul>
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
重要:为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一的
key属性。理想的key值是每项都有的唯一 id。不推荐使用索引作为key,因为当列表顺序变化时,索引也会变化,可能导致性能问题和状态错误。
v-for 也可以用来遍历对象的属性:
html复制<ul>
<li v-for="(value, name, index) in myObject">
{{ index }}. {{ name }}: {{ value }}
</li>
</ul>
事件处理
可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。
html复制<button v-on:click="counter += 1">Add 1</button>
v-on 可以简写为 @:
html复制<button @click="counter += 1">Add 1</button>
然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on 指令中是不可行的。因此 v-on 还可以接收一个需要调用的方法名称。
html复制<button @click="greet">Greet</button>
有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event 把它传入方法:
html复制<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
Vue 为 v-on 提供了事件修饰符,用于处理 DOM 事件细节:
.stop- 调用 event.stopPropagation().prevent- 调用 event.preventDefault().capture- 添加事件侦听器时使用 capture 模式.self- 只当事件是从侦听器绑定的元素本身触发时才触发回调.once- 只触发一次.passive- 以 { passive: true } 模式添加侦听器
html复制<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form @submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>
表单输入绑定
v-model 指令在表单 <input>、<textarea> 及 <select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。
html复制<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
v-model 会忽略所有表单元素的 value、checked、selected 属性的初始值,而总是将当前活动实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data 选项中声明初始值。
v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用
value属性和input事件; - checkbox 和 radio 使用
checked属性和change事件; - select 字段将
value作为 prop 并将change作为事件。
对于需要使用输入法(如中文、日文、韩文等)的语言,你会发现 v-model 不会在输入法组合文字过程中得到更新。如果你也想处理这个过程,请使用 input 事件。
v-model 还可以添加修饰符:
.lazy- 取代input监听change事件.number- 输入字符串转为有效的数字.trim- 输入首尾空格过滤
html复制<!-- 在"change"时而非"input"时更新 -->
<input v-model.lazy="msg">
<!-- 将用户的输入值转为数值类型 -->
<input v-model.number="age" type="number">
<!-- 自动过滤用户输入的首尾空白字符 -->
<input v-model.trim="msg">
2.4 计算属性和侦听器
计算属性
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:
html复制<div id="example">
{{ message.split('').reverse().join('') }}
</div>
在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理。
所以,对于任何复杂逻辑,你都应当使用计算属性。
javascript复制export default {
data() {
return {
message: 'Hello'
}
},
computed: {
// 计算属性的 getter
reversedMessage() {
// `this` 指向组件实例
return this.message.split('').reverse().join('')
}
}
}
然后我们可以在模板中像普通属性一样使用计算属性:
html复制<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
侦听器
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
javascript复制export default {
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// 每当 question 改变时,这个函数就会执行
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = 'Thinking...'
// 模拟异步请求
setTimeout(() => {
this.answer = 'The answer to "' + this.question + '" is... 42!'
}, 1000)
}
}
}
在这个示例中,使用 watch 选项允许我们执行异步操作(访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
3. Vue 组件系统
3.1 组件基础
组件是 Vue 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。
单文件组件
在 Vue 中,我们通常使用单文件组件(Single File Components, SFC)的方式来组织代码。一个典型的单文件组件如下:
html复制<template>
<div class="example">
{{ msg }}
</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
单文件组件包含三个部分:
<template>- 组件的模板<script>- 组件的逻辑<style>- 组件的样式
这种组织方式使得组件更加模块化和可维护。
组件注册
为了在模板中使用组件,必须先注册它。Vue 中有两种组件注册类型:全局注册和局部注册。
全局注册的组件可以在应用中的任何地方使用:
javascript复制// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
const app = createApp(App)
app.component('my-component', MyComponent)
app.mount('#app')
局部注册的组件只能在当前组件中使用:
javascript复制import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
}
Props
组件实例的作用域是孤立的。这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据。要让子组件使用父组件的数据,需要通过子组件的 props 选项。
子组件要显式地用 props 选项声明它期待获得的数据:
javascript复制export default {
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
}
为了指定 prop 的类型,可以用对象的形式列出 prop:
javascript复制export default {
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或其他构造函数
}
}
Prop 可以指定验证要求:
javascript复制export default {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default() {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator(value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].includes(value)
}
},
// 具有默认值的函数
propG: {
type: Function,
// 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数
default() {
return 'Default function'
}
}
}
}
自定义事件
子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件:
html复制<button @click="$emit('enlarge-text')">
Enlarge text
</button>
然后父组件可以监听这个事件:
html复制<blog-post @enlarge-text="postFontSize += 0.1"></blog-post>
有时候,你可能需要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on 的 .native 修饰符:
html复制<blog-post @click.native="handleClick"></blog-post>
3.2 组件通信
父子组件通信
父组件通过 props 向下传递数据给子组件,子组件通过 events 向上传递消息给父组件:
html复制<!-- 父组件 -->
<template>
<div>
<child-component
:message="parentMessage"
@child-event="handleChildEvent"
/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from parent'
}
},
methods: {
handleChildEvent(payload) {
console.log('Received from child:', payload)
}
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<p>{{ message }}</p>
<button @click="sendToParent">Send to Parent</button>
</div>
</template>
<script>
export default {
props: {
message: String
},
methods: {
sendToParent() {
this.$emit('child-event', { data: 'Hello from child' })
}
}
}
</script>
非父子组件通信
对于非父子关系的组件,可以使用以下几种方式通信:
- 事件总线:创建一个中央事件总线
javascript复制// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
// 组件A
EventBus.$emit('event-name', payload)
// 组件B
EventBus.$on('event-name', payload => {
// 处理事件
})
-
Vuex/Pinia:使用状态管理库共享状态
-
Provide/Inject:祖先组件通过
provide提供数据,后代组件通过inject注入数据
javascript复制// 祖先组件
export default {
provide() {
return {
sharedData: this.sharedData
}
},
data() {
return {
sharedData: 'Some shared data'
}
}
}
// 后代组件
export default {
inject: ['sharedData']
}
3.3 插槽
Vue 实现了一套内容分发的 API,将 <slot> 元素作为承载分发内容的出口。
基本用法
html复制<!-- 子组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 父组件 -->
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
作用域插槽
有时让插槽内容能够访问子组件中的数据是很有用的:
html复制<!-- 子组件 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
<slot :item="item" :index="index"></slot>
</li>
</ul>
<!-- 父组件 -->
<todo-list :items="todos">
<template v-slot:default="slotProps">
<span class="green">{{ slotProps.item.text }}</span>
</template>
</todo-list>
3.4 动态组件
通过 Vue 的 <component> 元素加一个特殊的 is 属性可以实现动态组件:
html复制<component :is="currentTabComponent"></component>
currentTabComponent 可以是一个已注册组件的名字,或者是一个组件的选项对象。
4. Vue 路由与状态管理
4.1 Vue Router
Vue Router 是 Vue.js 的官方路由管理器。它与 Vue.js 核心深度集成,让构建单页面应用变得易如反掌。
基本使用
javascript复制// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
在应用中使用路由:
javascript复制// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
在模板中使用:
html复制<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</div>
</template>
路由导航
除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
javascript复制// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })
路由守卫
路由守卫主要用来通过跳转或取消的方式守卫导航。
全局前置守卫:
javascript复制router.beforeEach((to, from, next) => {
// 必须调用 `next`
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
路由独享的守卫:
javascript复制const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from, next) => {
// 拒绝访问
next(false)
}
}
]
组件内的守卫:
javascript复制export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` ,因为当守卫执行时,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 在导航离开渲染该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
4.2 状态管理 - Pinia
Pinia 是 Vue 的存储库,它允许你跨组件/页面共享状态。Pinia 是 Vuex 的替代方案,具有更简单的 API 和更好的 TypeScript 支持。
基本使用
安装 Pinia:
bash复制npm install pinia
创建 store:
javascript复制// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// 也可以定义为
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
在组件中使用:
javascript复制import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
counter.count++
// 带自动补全 ✨
counter.$patch({ count: counter.count + 1 })
// 或使用 action
counter.increment()
},
}
State
State 是 store 的核心部分:
javascript复制const store = useStore()
store.count++ // 直接修改
store.$patch({ count: store.count + 1 }) // 使用 $patch 修改
store.$reset() // 重置 state
Getters
Getters 是 store 的计算属性:
javascript复制export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
// 使用其他 getter
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
})
Actions
Actions 相当于组件中的 methods:
javascript复制export const useStore = defineStore('main', {
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}!`)
} catch (error) {
showTooltip(error)
return error
}
},
},
})
5. Vue 3 新特性
5.1 Composition API
Composition API 是 Vue 3 引入的一组 API,允许我们使用函数式风格编写组件逻辑。它解决了 Options API 在复杂组件中代码组织的问题。
setup 函数
setup 是一个新的组件选项,它是 Composition API 的入口点:
javascript复制import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const state = reactive({ name: 'Vue' })
function increment() {
count.value++
}
// 暴露给模板
return {
count,
state,
increment
}
}
}
响应式基础
ref 和 reactive 是创建响应式数据的两种主要方式:
javascript复制import { ref, reactive } from 'vue'
const count = ref(0)
console.log(count.value) // 0
const state = reactive({
count: 0,
name: 'Vue'
})
console.log(state.count) // 0
计算属性和侦听器
javascript复制import { ref, computed, watch } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})
生命周期钩子
Composition API 提供了对应的生命周期钩子函数:
javascript复制import { onMounted, onUpdated, onUnmounted } from 'vue'
setup() {
onMounted(() => {
console.log('component is mounted!')
})
onUpdated(() => {
console.log('component is updated!')
})
onUnmounted(() => {
console.log('component is unmounted!')
})
}
5.2 Script Setup
<script setup> 是在单文件组件中使用 Composition API 的编译时语法糖:
html复制<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
5.3 Teleport
<Teleport> 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染 HTML:
html复制<template>
<button @click="modalOpen = true">Open Modal</button>
<Teleport to="body">
<div v-if="modalOpen" class="modal">
<p>Hello from the modal!</p>
<button @click="modalOpen = false">Close</button>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const modalOpen = ref(false)
</script>
5.4 Suspense
<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理:
html复制<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 "正在加载中" -->
<template #fallback>
Loading...
</template>
</Suspense>
6. Vue 性能优化
6.1 代码层面优化
-
合理使用 v-if 和 v-show:
v-if是"真正"的条件渲染,有更高的切换开销v-show只是简单地切换 CSS 的display属性,有更高的初始渲染开销- 需要频繁切换时使用
v-show,运行时条件很少改变时使用v-if
-
为 v-for 设置 key:
- 使用唯一且稳定的 id 作为 key
- 避免使用数组索引作为 key
-
避免 v-if 和 v-for 一起使用:
- Vue 2 中
v-for优先级高于v-if,会导致不必要的计算 - 解决方案:使用计算属性先过滤数据
- Vue 2 中
-
使用计算属性缓存结果:
- 计算属性基于它们的响应式依赖进行缓存
- 只在相关响应式依赖发生改变时才会重新计算
-
合理拆分组件:
- 将大型组件拆分为更小的、可复用的组件
- 减少单个组件的复杂度,提高可维护性
6.2 打包优化
-
路由懒加载:
- 使用动态导入语法
() => import('./views/About.vue') - 将不同路由对应的组件分割成不同的代码块
- 使用动态导入语法
-
第三方库按需引入:
- 使用
unplugin-vue-components等工具自动按需引入组件库 - 避免引入整个库,只引入需要的部分
- 使用
-
使用 Vite 替代 Webpack:
- Vite 利用浏览器原生 ES 模块支持,提供极速的开发服务器启动
- 生产环境使用 Rollup 打包,生成更高效的代码
-
开启 Gzip 压缩:
- 使用
compression-webpack-plugin或 Vite 的对应插件 - 显著减少资源体积,提高加载速度
- 使用
-
CDN 加速:
- 将 Vue、Vue Router、Pinia 等库通过 CDN 引入
- 减少打包体积,利用浏览器缓存
6.3 运行时优化
-
使用 keep-alive 缓存组件:
- 包裹动态组件时,会缓存不活动的组件实例
- 避免重复渲染,保留组件状态
-
虚拟滚动优化长列表:
- 使用
vue-virtual-scroller等库实现虚拟滚动 - 只渲染可见区域的内容,大幅提升性能
- 使用
-
防抖和节流:
- 对频繁触发的事件(如 scroll、resize、input)使用防抖或节流
- 减少不必要的计算和渲染
-
合理使用 v-once:
- 只渲染元素和组件一次,并跳过之后的更新
- 适用于静态内容优化
-
使用生产环境构建:
- 生产环境构建会移除警告、调试代码,并进行代码压缩
- 使用
vue.config.js或 Vite 配置进行优化
7. Vue 生态与工具链
7.1 构建工具
-
Vite:
- 下一代前端开发与构建工具
- 极速的服务启动和热更新
- 基于原生 ES 模块,按需编译
-
Vue CLI:
- Vue 官方脚手架工具
- 基于 Webpack,提供丰富的插件系统
- 适合传统项目
7.2 UI 组件库
-
Element Plus:
- 基于 Vue 3 的桌面端组件库
- 丰富的组件,良好的文档
-
Ant Design Vue:
- Ant Design 的 Vue 实现
- 企业级设计语言和组件
-
Vuetify:
- Material Design 风格的组件库
- 全面的组件和布局系统
7.3 测试工具
-
Vitest:
- 基于 Vite 的单元测试框架
- 与 Vite 共享配置,速度快
-
Vue Test Utils:
- Vue 官方测试工具