1. Vue.js 基础介绍
1.1 什么是 Vue.js
Vue.js(简称 Vue)是一个用于构建用户界面的渐进式 JavaScript 框架。与其他框架不同,Vue 被设计为可以逐步采用。Vue 的核心库只关注视图层,易于上手,便于与其他库或既有项目整合。
Vue 由尤雨溪(Evan You)在 2014 年创建。尤雨溪曾在 Google 工作,参与了 AngularJS 的开发,后来他希望提取 Angular 中他认为精华的部分,构建一个更轻量级的框架,于是创建了 Vue。
1.2 Vue.js 的特点
-
渐进式框架:可以逐步将 Vue 集成到项目中,无需一次全部采用。
-
响应式系统:Vue 提供了响应式且组件化的视图组件,当数据变化时,视图会自动更新。
-
虚拟 DOM:Vue 使用虚拟 DOM(Virtual DOM)技术提高渲染性能。
-
组件化开发:鼓励将应用拆分为独立可复用的组件,构建出大型应用。
-
轻量级:Vue 的体积小巧,压缩后仅约 20KB(Vue 2)或 10KB(Vue 3)。
-
易学易用:相较于其他前端框架,Vue 的学习曲线更平缓,API 设计简单直观。
-
丰富的工具链:Vue 提供了完整的开发工具链,如 Vue CLI、Vite、DevTools 等。
1.3 Vue.js 应用场景
Vue 可适用于多种场景:
-
单页应用(SPA):使用 Vue Router 构建单页应用,避免页面刷新提升用户体验。
-
多页应用:将 Vue 组件集成到传统的多页面应用中。
-
移动端应用:结合 Cordova、Capacitor 或 NativeScript 构建移动应用。
-
桌面应用:结合 Electron 构建桌面应用。
-
服务端渲染:使用 Nuxt.js 或手动配置 Vue SSR。
-
静态站点生成:使用 VuePress 或 Gridsome 生成静态站点。
1.4 Vue.js 版本历史
- Vue 1.0:2015 年 10 月发布,奠定了 Vue 的基础架构。
- Vue 2.0:2016 年 9 月发布,引入虚拟 DOM,提升性能。
- Vue 2.6:2019 年 2 月发布,添加了 Composition API RFC 等特性。
- Vue 3.0:2020 年 9 月发布,全新的架构,更好的性能和更小的体积。
- Vue 3.2:2021 年 8 月发布,引入
<script setup>
语法。 - Vue 3.3:2023 年 5 月发布,改进了 TypeScript 支持和宏性能。
- Vue 3.4:2023 年 12 月发布,改进了渲染器和编译器性能。
2. Vue 2 与 Vue 3 比较
2.1 核心架构变化
特性 | Vue 2 | Vue 3 |
---|---|---|
响应式系统 | Object.defineProperty | Proxy |
代码组织 | Options API 为主 | Options API + Composition API |
模板编译 | 模板编译为渲染函数 | 改进的模板编译策略,更好的静态提升 |
虚拟 DOM | 基本实现 | 重写,更快的挂载和更新速度 |
TypeScript 支持 | 有限支持 | 完全支持,代码库用 TS 重写 |
Tree-Shaking | 有限支持 | 全面支持,更小的打包体积 |
2.2 API 变化
Vue 3 新增的 API:
- Composition API(
setup
,ref
,reactive
等) - Teleport 组件
- Fragments(片段)
- Suspense 组件
createApp
替代new Vue()
- 多个根节点支持
emits
选项
Vue 3 移除的 API:
$on
,$off
,$once
事件 API- 过滤器(Filters)
$children
实例属性$destroy
实例方法
2.3 性能对比
Vue 3 相比 Vue 2 在性能上有显著提升:
-
更小的包体积:Vue 3 核心库体积比 Vue 2 减小了约 41%,最小化和压缩后仅约 10KB。
-
更快的初始渲染:Vue 3 初始渲染速度比 Vue 2 快约 55%。
-
更高效的更新:由于优化的虚拟 DOM 和编译时提示,Vue 3 的更新性能比 Vue 2 快约 133%。
-
内存占用更低:Vue 3 减少了约 54% 的内存使用量。
2.4 生态系统适配
Vue 3 发布后,主要生态系统库逐步适配:
- Vue Router:4.x 版本支持 Vue 3
- Vuex:4.x 版本支持 Vue 3
- Pinia:新一代状态管理库,专为 Vue 3 设计
- Vite:新一代构建工具,原生支持 Vue 3
- Nuxt:3.x 版本支持 Vue 3
- UI 库:Element Plus、Vuetify 3、Quasar 2 等
2.5 迁移策略
从 Vue 2 迁移到 Vue 3 的建议策略:
- 渐进式迁移:使用 Vue 2.7(带有部分 Vue 3 特性)作为过渡。
- 使用迁移构建版本:Vue 3 提供了兼容 Vue 2 API 的构建版本。
- 使用迁移工具:Vue 团队提供了迁移助手工具。
- 分阶段迁移:先更新依赖,再更新代码风格,最后优化架构。
- 新项目直接使用 Vue 3:新项目建议直接采用 Vue 3。
3. 环境搭建与项目结构
3.1 安装 Vue
有多种方式可以在项目中使用 Vue:
3.1.1 直接引入
最简单的方法是通过 CDN 引入 Vue:
<!-- Vue 2 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script><!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.37/dist/vue.global.js"></script>
对于生产环境,应使用压缩版本:
<!-- Vue 2 生产版本 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script><!-- Vue 3 生产版本 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.37"></script>
3.1.2 使用 npm
推荐使用 npm 管理依赖:
# Vue 2
npm install vue@2# Vue 3
npm install vue@next
3.1.3 使用 Vue CLI
Vue CLI 是一个官方的项目脚手架工具:
# 安装 Vue CLI
npm install -g @vue/cli# 创建一个新项目
vue create my-project# 选择 Vue 2 或 Vue 3 作为默认预设
3.1.4 使用 Vite
Vite 是一个新一代的前端构建工具,由 Vue 团队开发:
# 使用 npm
npm init vite@latest my-vue-app -- --template vue# 使用 yarn
yarn create vite my-vue-app --template vue# 使用 pnpm
pnpm create vite my-vue-app -- --template vue
3.2 项目结构
典型的 Vue 项目结构(基于 Vue CLI 或 Vite 创建):
my-vue-project/
├── .vscode/ # VSCode 配置
├── node_modules/ # npm 依赖
├── public/ # 静态资源,不经过 webpack 处理
│ ├── favicon.ico # 网站图标
│ └── index.html # HTML 模板
├── src/ # 源代码
│ ├── assets/ # 资源文件(会被打包)
│ ├── components/ # 组件
│ ├── router/ # 路由配置(Vue Router)
│ ├── store/ # 状态管理(Vuex/Pinia)
│ ├── views/ # 视图/页面组件
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── .browserslistrc # 浏览器兼容性配置
├── .eslintrc.js # ESLint 配置
├── .gitignore # Git 忽略文件
├── babel.config.js # Babel 配置
├── package.json # 项目配置和依赖
├── README.md # 项目说明文档
└── vue.config.js # Vue CLI 配置文件
Vite 项目结构略有不同,通常没有 vue.config.js
,而是 vite.config.js
。
3.3 开发工具
有多种工具可帮助 Vue 开发:
3.3.1 Vue DevTools
Vue DevTools 是一个浏览器扩展,可以帮助调试 Vue 应用:
- 检查组件树
- 查看组件状态
- 跟踪事件
- 分析性能
- 时间旅行调试(Vuex/Pinia)
3.3.2 IDE 支持
Visual Studio Code 是最受欢迎的 Vue 开发 IDE,推荐以下扩展:
- Volar (Vue 3)
- Vetur (Vue 2)
- ESLint
- Prettier
- Vue VSCode Snippets
3.3.3 其他工具
- Vue CLI GUI:Vue CLI 的图形界面
- Vue Devtools Standalone:独立应用版 Vue Devtools
- Vite:快速的开发服务器和构建工具
- Nuxt DevTools:Nuxt.js 开发工具
3.4 配置文件
3.4.1 Vue CLI 配置
vue.config.js
文件可配置 Vue CLI 项目:
module.exports = {publicPath: process.env.NODE_ENV === 'production' ? '/my-app/' : '/',outputDir: 'dist',assetsDir: 'static',productionSourceMap: false,devServer: {port: 8080,proxy: {'/api': {target: 'http://localhost:3000',changeOrigin: true}}},css: {loaderOptions: {sass: {additionalData: `@import "@/styles/variables.scss";`}}}
}
3.4.2 Vite 配置
vite.config.js
文件配置 Vite 项目:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'export default defineConfig({plugins: [vue()],resolve: {alias: {'@': path.resolve(__dirname, './src')}},server: {port: 3000,proxy: {'/api': {target: 'http://localhost:8080',changeOrigin: true}}},build: {outDir: 'dist',assetsDir: 'assets',sourcemap: false}
})
4. Vue 核心概念
4.1 声明式渲染
Vue.js 的核心是声明式渲染系统,允许我们声明式地将数据渲染为 DOM:
<div id="app">{{ message }}
</div>
// Vue 2
new Vue({el: '#app',data: {message: 'Hello Vue!'}
})// Vue 3
Vue.createApp({data() {return {message: 'Hello Vue!'}}
}).mount('#app')
声明式渲染的优势:
- 代码简洁易读
- 关注数据而非 DOM 操作
- 自动更新视图
- 可维护性更高
4.2 响应式系统
Vue 的响应式系统使得数据与视图保持同步:
4.2.1 Vue 2 响应式原理
Vue 2 使用 Object.defineProperty
实现响应式:
let data = { message: 'Hello' }
let vm = {}Object.defineProperty(vm, 'message', {get() {return data.message},set(newValue) {data.message = newValueupdateView() // 更新视图}
})function updateView() {console.log('视图更新:', vm.message)
}// 修改属性触发视图更新
vm.message = 'Hello Vue!'
Vue 2 响应式系统限制:
- 无法检测到对象属性的添加或删除
- 无法检测数组索引的变化和长度的变化
- 需要使用 Vue.set() 或 this.$set() 添加新属性
4.2.2 Vue 3 响应式原理
Vue 3 使用 Proxy 实现响应式:
let data = { message: 'Hello' }const handler = {get(target, key) {track(target, key) // 依赖跟踪return Reflect.get(target, key)},set(target, key, value) {const result = Reflect.set(target, key, value)trigger(target, key) // 触发更新return result}
}const proxy = new Proxy(data, handler)// 修改属性触发视图更新
proxy.message = 'Hello Vue 3!'
// 添加新属性也能触发视图更新
proxy.newProperty = 'New Value'
Vue 3 响应式系统优势:
- 可以检测对象属性的添加和删除
- 可以检测数组索引和长度的变化
- 可以监听 Map, Set, WeakMap, WeakSet
- 性能更好,消耗更少
4.3 指令系统
Vue 指令是带有 v-
前缀的特殊 HTML 属性,用于在模板中应用特殊的响应式行为:
4.3.1 常用内置指令
v-bind
: 动态绑定属性v-on
: 绑定事件监听器v-if
: 条件性渲染元素v-for
: 基于数组渲染列表v-model
: 表单输入绑定v-show
: 切换元素的可见性v-slot
: 插槽内容分发v-once
: 一次性插值v-pre
: 跳过编译v-cloak
: 隐藏未编译的模板v-text
: 设置文本内容v-html
: 设置 HTML 内容
4.3.2 指令参数和修饰符
指令可以带参数和修饰符:
<!-- 参数 -->
<a v-bind:href="url">链接</a>
<button v-on:click="doSomething">点击</button><!-- 修饰符 -->
<form v-on:submit.prevent="onSubmit">表单</form>
<input v-model.trim="message">
4.3.3 自定义指令
可以注册自定义指令:
Vue 2:
// 全局注册
Vue.directive('focus', {inserted: function(el) {el.focus()}
})// 局部注册
new Vue({directives: {focus: {inserted: function(el) {el.focus()}}}
})
Vue 3:
// 全局注册
const app = Vue.createApp({})
app.directive('focus', {mounted(el) {el.focus()}
})// 局部注册
export default {directives: {focus: {mounted(el) {el.focus()}}}
}
自定义指令钩子函数:
Vue 2 | Vue 3 | 描述 |
---|---|---|
bind | beforeMount | 指令绑定到元素时调用 |
inserted | mounted | 元素插入父节点时调用 |
update | - | 元素更新时调用(去除) |
componentUpdated | updated | 组件和子组件更新时调用 |
unbind | unmounted | 指令与元素解绑时调用 |
4.4 生命周期
Vue 组件有一系列的生命周期钩子,允许在特定阶段执行代码:
4.4.1 Vue 2 生命周期
new Vue({beforeCreate() {// 实例初始化后,数据观测和事件配置之前},created() {// 实例创建完成后调用,此时数据已经可用},beforeMount() {// 挂载开始之前被调用,render 函数首次调用},mounted() {// 实例挂载到 DOM 后调用,可访问 DOM 元素},beforeUpdate() {// 数据更改导致虚拟 DOM 重新渲染前调用},updated() {// 虚拟 DOM 重新渲染后调用},activated() {// keep-alive 组件激活时调用},deactivated() {// keep-alive 组件停用时调用},beforeDestroy() {// 实例销毁前调用},destroyed() {// 实例销毁后调用},errorCaptured() {// 捕获子孙组件错误时调用}
})
4.4.2 Vue 3 生命周期
export default {// 选项式 API 生命周期beforeCreate() { /* ... */ },created() { /* ... */ },beforeMount() { /* ... */ },mounted() { /* ... */ },beforeUpdate() { /* ... */ },updated() { /* ... */ },beforeUnmount() { /* ... */ }, // 替代 beforeDestroyunmounted() { /* ... */ }, // 替代 destroyedactivated() { /* ... */ },deactivated() { /* ... */ },errorCaptured() { /* ... */ },renderTracked() { /* ... */ }, // 新增:跟踪虚拟 DOM 重新渲染时调用renderTriggered() { /* ... */ } // 新增:虚拟 DOM 重新渲染被触发时调用
}
4.4.3 Vue 3 组合式 API 生命周期钩子
import { onBeforeMount, onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted,onActivated,onDeactivated,onErrorCaptured,onRenderTracked,onRenderTriggered
} from 'vue'export default {setup() {// 注意:没有 beforeCreate 和 created 对应的钩子// setup 本身在 beforeCreate 之后、created 之前执行onBeforeMount(() => { /* ... */ })onMounted(() => { /* ... */ })onBeforeUpdate(() => { /* ... */ })onUpdated(() => { /* ... */ })onBeforeUnmount(() => { /* ... */ })onUnmounted(() => { /* ... */ })onActivated(() => { /* ... */ })onDeactivated(() => { /* ... */ })onErrorCaptured(() => { /* ... */ })onRenderTracked(() => { /* ... */ })onRenderTriggered(() => { /* ... */ })}
}
4.5 Vue 实例属性和方法
Vue 实例提供了许多有用的属性和方法:
4.5.1 Vue 2 实例属性和方法
实例属性:
$data
: Vue 实例监视的数据对象$props
: 当前组件接收的 props$el
: Vue 实例使用的根 DOM 元素$options
: 当前 Vue 实例的初始化选项$parent
: 父实例$root
: 根 Vue 实例$children
: 当前实例的直接子组件$slots
: 访问插槽内容$scopedSlots
: 访问作用域插槽$refs
: 持有注册过 ref 的所有 DOM 元素和组件实例$isServer
: 当前 Vue 实例是否运行于服务端$attrs
: 包含父作用域中非 prop 的属性绑定$listeners
: 包含父作用域中的事件监听器
实例方法:
$watch()
: 观察 Vue 实例变化的一个表达式或计算属性函数$set()
: 全局 Vue.set 的别名$delete()
: 全局 Vue.delete 的别名$on()
: 监听当前实例上的自定义事件$once()
: 监听一个自定义事件,但只触发一次$off()
: 移除自定义事件监听器$emit()
: 触发当前实例上的事件$mount()
: 手动挂载一个未挂载的实例$forceUpdate()
: 强制 Vue 实例重新渲染$nextTick()
: 将回调延迟到下次 DOM 更新循环之后执行$destroy()
: 完全销毁一个实例
4.5.2 Vue 3 实例属性和方法
Vue 3 移除了部分实例属性和方法,如 $on
, $off
, $once
, $children
等。其余大部分属性保持不变,但获取方式可能不同:
// 选项式 API 中访问实例属性
export default {mounted() {console.log(this.$data)console.log(this.$el)}
}// 组合式 API 中访问实例属性
import { getCurrentInstance } from 'vue'export default {setup() {const instance = getCurrentInstance()// 在 setup 中访问实例(仅在开发环境可用)console.log(instance.data)console.log(instance.proxy.$el) // 需要通过 proxy 访问}
}
5. Vue 实例详解
5.1 创建 Vue 实例
5.1.1 Vue 2 创建实例
在 Vue 2 中,通过 new Vue()
创建实例:
const vm = new Vue({// 选项
})
5.1.2 Vue 3 创建应用
在 Vue 3 中,通过 createApp()
创建应用实例:
import { createApp } from 'vue'
import App from './App.vue'const app = createApp(App)
app.mount('#app')
Vue 3 的设计更加模块化,全局 API 移至应用实例:
// Vue 2
Vue.component('my-component', { /* ... */ })
Vue.directive('my-directive', { /* ... */ })
Vue.mixin({ /* ... */ })// Vue 3
const app = createApp(App)
app.component('my-component', { /* ... */ })
app.directive('my-directive', { /* ... */ })
app.mixin({ /* ... */ })
5.2 数据与方法
5.2.1 data 选项
Vue 实例的核心是 data
选项,它是一个对象或函数:
// Vue 2 根实例:对象语法
new Vue({data: {message: 'Hello'}
})// Vue 2 组件:函数语法
Vue.component('my-component', {data() {return {message: 'Hello'}}
})// Vue 3:始终使用函数语法
export default {data() {return {message: 'Hello'}}
}
为什么组件的 data 必须是函数? 为了确保每个组件实例有独立的数据副本,防止多个组件实例共享同一个数据对象。
5.2.2 methods 选项
methods
选项用于添加方法:
export default {data() {return {count: 0}},methods: {increment() {this.count++},decrement() {this.count--},reset() {this.count = 0}}
}
方法中的 this
自动绑定到 Vue 实例。
注意事项:
- 不要使用箭头函数定义 method,因为箭头函数没有自己的
this
- 方法可以在模板中直接调用,也可以用于事件处理
5.3 计算属性
计算属性是基于响应式依赖缓存的。只有相关依赖发生变化时才会重新计算:
export default {data() {return {firstName: 'John',lastName: 'Doe'}},computed: {// 基本用法fullName() {return this.firstName + ' ' + this.lastName},// 带 getter 和 setterfullNameWithSetter: {get() {return this.firstName + ' ' + this.lastName},set(newValue) {const names = newValue.split(' ')this.firstName = names[0]this.lastName = names[names.length - 1]}}}
}
计算属性的优势:
- 缓存基于依赖,只有依赖变化时才重新计算
- 声明式编程,更简洁易读
- 自动跟踪依赖关系
5.4 侦听器
侦听器用于观察和响应 Vue 实例上的数据变动:
export default {data() {return {question: '',answer: 'Questions usually contain a question mark. ;-)'}},watch: {// 基本用法question(newQuestion, oldQuestion) {if (newQuestion.includes('?')) {this.getAnswer()}},// 深度侦听someObject: {handler(newValue, oldValue) {console.log('someObject changed')},deep: true},// 立即执行otherProperty: {handler(newValue, oldValue) {console.log('otherProperty changed')},immediate: true}},methods: {getAnswer() {this.answer = 'Thinking...'setTimeout(() => {this.answer = 'I think you should...'}, 1000)}}
}
侦听器特性:
- 可以执行异步操作
- 可以访问新值和旧值
- 可以深度侦听对象变化
- 可以在创建后立即执行
5.5 组件间通信
Vue 提供了多种组件通信方式:
5.5.1 Props 向下传递数据
父组件向子组件传递数据:
// 子组件
export default {props: {title: String,likes: Number,isPublished: Boolean,commentIds: Array,author: Object,callback: Function,contactsPromise: Promise}
}// 父组件
<blog-posttitle="My journey with Vue":likes="42":is-published="true"
></blog-post>
5.5.2 自定义事件向上传递数据
子组件向父组件传递数据:
// 子组件
export default {methods: {incrementCounter() {this.$emit('increment', 1)}}
}// 父组件
<button-counter @increment="incrementTotal"></button-counter>
5.5.3 其他通信方式
- refs:直接访问子组件
- provide/inject:祖先组件向所有子孙组件注入数据
- EventBus:Vue 2 中创建一个事件总线(不推荐)
- Vuex/Pinia:专门的状态管理解决方案
- mitt/tiny-emitter:Vue 3 中的事件库替代 EventBus
6. 模板语法与渲染
6.1 插值
6.1.1 文本插值
使用双大括号语法插入文本:
<span>Message: {{ msg }}</span>
6.1.2 原始 HTML
使用 v-html
指令插入 HTML:
<div v-html="rawHtml"></div>
安全警告:使用 v-html
可能导致 XSS 攻击,只对可信内容使用,永不用于用户提供的内容。
6.1.3 属性绑定
使用 v-bind
指令绑定 HTML 属性:
<div v-bind:id="dynamicId"></div>
<!-- 简写 -->
<div :id="dynamicId"></div><!-- 布尔属性 -->
<button :disabled="isButtonDisabled">Button</button><!-- 多个属性绑定 -->
<div v-bind="{ id: 'container', class: 'wrapper' }"></div>
6.2 JavaScript 表达式
Vue 模板中支持完整的 JavaScript 表达式:
{{ number + 1 }}{{ ok ? 'YES' : 'NO' }}{{ message.split('').reverse().join('') }}<div :id="`list-${id}`"></div>
限制:每个绑定只能包含单个表达式,不支持语句或控制流。
6.3 指令详解
指令是带有 v-
前缀的特殊 attribute,指令的值是单个 JavaScript 表达式。
6.3.1 参数
指令可以接收参数,在指令名之后以冒号表示:
<a v-bind:href="url">链接</a>
<button v-on:click="doSomething">点击</button>
6.3.2 动态参数
可以用方括号括起来的 JavaScript 表达式作为指令的参数:
<a v-bind:[attributeName]="url">链接</a>
<button v-on:[eventName]="doSomething">点击</button>
6.3.3 修饰符
修饰符是以点开头的特殊后缀,表示指令应该以特殊方式绑定:
<!-- 阻止默认行为 -->
<form v-on:submit.prevent="onSubmit"></form><!-- 键盘事件 -->
<input v-on:keyup.enter="submit"><!-- 表单修饰符 -->
<input v-model.trim="msg">
6.4 缩写
Vue 为最常用的指令提供了缩写:
<!-- 完整语法 -->
<a v-bind:href="url">链接</a>
<button v-on:click="doSomething">点击</button><!-- 缩写 -->
<a :href="url">链接</a>
<button @click="doSomething">点击</button><!-- 动态参数缩写 -->
<a :[key]="url">链接</a>
<button @[event]="doSomething">点击</button>
6.5 模板中的条件与循环
6.5.1 条件渲染
使用 v-if
, v-else-if
, v-else
进行条件渲染:
<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
控制元素显示:
<h1 v-show="ok">Hello!</h1>
v-if
与 v-show
对比:
v-if
是"真正"的条件渲染,元素会被销毁和重建v-show
只是切换 CSSdisplay
属性v-if
有更高的切换开销,v-show
有更高的初始渲染开销- 频繁切换使用
v-show
,条件很少改变使用v-if
6.5.2 列表渲染
使用 v-for
基于数组渲染列表:
<ul><li v-for="(item, index) in items" :key="item.id">{{ index }} - {{ item.text }}</li>
</ul>
v-for
也可以遍历对象属性:
<ul><li v-for="(value, key, index) in object" :key="key">{{ index }}. {{ key }}: {{ value }}</li>
</ul>
v-for
与 v-if
不应在同一元素上使用,因为 v-for
比 v-if
优先级更高。
6.6 过滤器 (Vue 2)
Vue 2 支持过滤器,可用于文本格式化:
<!-- 在双花括号中 -->
{{ message | capitalize }}<!-- 在 v-bind 中 -->
<div v-bind:id="rawId | formatId"></div>
filters: {capitalize(value) {if (!value) return ''value = value.toString()return value.charAt(0).toUpperCase() + value.slice(1)}
}
注意:Vue 3 已移除过滤器,推荐使用方法或计算属性代替。
7. 计算属性与侦听器
7.1 计算属性详解
计算属性是基于其响应式依赖进行缓存的。
7.1.1 基本用法
export default {data() {return {message: 'Hello'}},computed: {// 计算属性的 getterreversedMessage() {// `this` 指向组件实例return this.message.split('').reverse().join('')}}
}
7.1.2 计算属性缓存 vs 方法
计算属性和方法的区别:
- 计算属性基于依赖缓存,依赖不变时不会重新计算
- 方法在每次重新渲染时都会执行
- 对于计算开销大的操作,应优先使用计算属性
// 计算属性(有缓存)
computed: {expensiveOperation() {console.log('Computing expensive operation')return this.items.filter(item => item.important).map(...)}
}// 方法(无缓存)
methods: {expensiveMethod() {console.log('Running expensive method')return this.items.filter(item => item.important).map(...)}
}
7.1.3 计算属性的 setter
计算属性默认只有 getter,但也可以提供 setter:
export default {data() {return {firstName: 'John',lastName: 'Doe'}},computed: {fullName: {// getterget() {return this.firstName + ' ' + this.lastName},// setterset(newValue) {const names = newValue.split(' ')this.firstName = names[0]this.lastName = names[names.length - 1] || ''}}}
}
使用 setter:
// 此时会调用 setter: this.firstName 和 this.lastName 会相应更新
this.fullName = 'Jane Smith'
7.2 侦听器详解
侦听器适用于需要在数据变化时执行异步或开销较大的操作。
7.2.1 基本用法
export default {data() {return {question: '',answer: 'Questions usually contain a question mark. ;-)'}},watch: {// 每当 question 发生变化时,该函数将会执行question(newQuestion, oldQuestion) {if (newQuestion.includes('?')) {this.getAnswer()}}},methods: {getAnswer() {this.answer = 'Thinking...'axios.get('https://api.example.com/answer').then(response => {this.answer = response.data.answer}).catch(error => {this.answer = 'Error! Could not reach the API. ' + error})}}
}
7.2.2 深度侦听
默认情况下,侦听器只会监听顶层属性的变化。使用 deep
选项进行深度侦听:
export default {data() {return {user: {name: 'John',profile: {age: 30,address: {city: 'New York'}}}}},watch: {user: {handler(newValue, oldValue) {console.log('User object changed')},deep: true},// 也可以直接监听嵌套属性'user.profile.age'(newValue, oldValue) {console.log('Age changed from', oldValue, 'to', newValue)}}
}
7.2.3 立即执行
使用 immediate
选项可以使侦听器在创建时立即执行:
watch: {question: {handler(newQuestion, oldQuestion) {this.answer = 'Waiting for you to stop typing...'this.debouncedGetAnswer()},immediate: true}
}
7.2.4 停止侦听
在选项式 API 中,侦听器会在组件销毁时自动停止。如果需要提前停止,可以使用 $watch
返回的取消函数:
const unwatch = this.$watch('question', (newQuestion) => {// ...
})// 当不再需要观察时,调用取消函数
unwatch()
在组合式 API 中,watch
函数返回停止侦听的函数:
import { ref, watch } from 'vue'export default {setup() {const question = ref('')// 开始侦听const stopWatching = watch(question, (newQuestion) => {// ...})// 停止侦听stopWatching()}
}
7.3 计算属性 vs 侦听器
计算属性和侦听器的选择原则:
-
计算属性:适用于根据现有数据派生出新数据
- 更加声明式
- 有缓存机制
- 适合同步计算
-
侦听器:适用于响应数据变化
- 支持异步操作
- 可以执行副作用(如修改 DOM、发送网络请求)
- 可以访问变化前后的值
- 可以监视多个数据源
7.3.1 何时使用计算属性
// 使用计算属性(更好)
computed: {fullName() {return this.firstName + ' ' + this.lastName}
}// 使用侦听器(不推荐)
watch: {firstName(newVal) {this.fullName = newVal + ' ' + this.lastName},lastName(newVal) {this.fullName = this.firstName + ' ' + newVal}
}
7.3.2 何时使用侦听器
// 使用侦听器
watch: {searchQuery(newQuery) {this.isLoading = truethis.debouncedGetResults(newQuery)}
},
methods: {// 防抖函数debounce(fn, delay) {let timeoutreturn function(...args) {clearTimeout(timeout)timeout = setTimeout(() => fn.apply(this, args), delay)}},created() {this.debouncedGetResults = this.debounce(this.getResults, 300)},getResults(query) {axios.get('/api/search', { params: { query } }).then(response => {this.results = response.datathis.isLoading = false})}
}
8. Class 与 Style 绑定
8.1 绑定 HTML Class
8.1.1 对象语法
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
data() {return {isActive: true,hasError: false}
}
渲染结果:
<div class="active"></div>
也可以绑定一个对象:
<div :class="classObject"></div>
data() {return {classObject: {active: true,'text-danger': false}}
}
或使用计算属性:
computed: {classObject() {return {active: this.isActive && !this.error,'text-danger': this.error && this.error.type === 'fatal'}}
}
8.1.2 数组语法
<div :class="[activeClass, errorClass]"></div>
data() {return {activeClass: 'active',errorClass: 'text-danger'}
}
渲染结果:
<div class="active text-danger"></div>
数组中使用条件:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
更简洁的方式是在数组中使用对象语法:
<div :class="[{ active: isActive }, errorClass]"></div>
8.1.3 与组件结合使用
当在自定义组件上使用 class
时,这些类将被添加到组件的根元素上:
<!-- 假设这是父组件模板 -->
<my-component class="baz"></my-component>
// 组件定义
Vue.component('my-component', {template: '<p class="foo bar">Hi</p>'
})
渲染结果:
<p class="foo bar baz">Hi</p>
8.2 绑定内联样式
8.2.1 对象语法
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data() {return {activeColor: 'red',fontSize: 30}
}
通常绑定一个样式对象会更清晰:
<div :style="styleObject"></div>
data() {return {styleObject: {color: 'red',fontSize: '13px'}}
}
8.2.2 数组语法
可以将多个样式对象应用到同一个元素上:
<div :style="[baseStyles, overridingStyles]"></div>
data() {return {baseStyles: {color: 'blue',fontSize: '16px'},overridingStyles: {fontWeight: 'bold',border: '1px solid black'}}
}
8.2.3 自动前缀
使用 :style
时,Vue 会自动为需要添加浏览器前缀的 CSS 属性添加适当的前缀:
<div :style="{ display: 'flex' }"></div>
渲染结果(取决于浏览器):
<div style="display: -webkit-flex; display: flex;"></div>
8.2.4 多重值
从 Vue 2.3 开始,可以为样式属性提供一个包含多个值的数组,只会渲染数组中浏览器支持的最后一个值:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
渲染结果(在支持 flex 的浏览器中):
<div style="display: flex;"></div>
9. 条件渲染
9.1 v-if 指令
v-if
指令用于条件性地渲染一块内容,只有当表达式为 truthy 时,内容才会被渲染:
<h1 v-if="awesome">Vue is awesome!</h1>
9.2 v-else 和 v-else-if
v-else
和 v-else-if
必须紧跟在 v-if
或 v-else-if
元素之后:
<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>
9.3 在 <template>
上使用 v-if
对于需要同时条件渲染多个元素,可以使用不可见的 <template>
元素包裹,最终渲染结果将不包含 <template>
元素:
<template v-if="ok"><h1>Title</h1><p>Paragraph 1</p><p>Paragraph 2</p>
</template>
9.4 v-show 指令
v-show
也用于条件性显示元素,但元素始终会被渲染并保留在 DOM 中,只是简单地切换 CSS 的 display
属性:
<h1 v-show="ok">Hello!</h1>
注意:v-show
不支持在 <template>
元素上使用,也不支持 v-else
。
9.5 v-if vs v-show
-
v-if:
- "真正"的条件渲染(创建和销毁元素)
- 有更高的切换开销
- 在运行时条件很少改变时使用
- 支持
<template>
,v-else
和v-else-if
-
v-show:
- 元素始终被渲染,只是切换 CSS
display
属性 - 有更高的初始渲染开销
- 需要频繁切换时使用
- 不支持
<template>
和v-else
- 元素始终被渲染,只是切换 CSS
9.6 v-if 与 v-for 一起使用
不推荐同时使用 v-if
和 v-for
:当它们同时存在于一个元素上时,v-for
的优先级高于 v-if
。
下面的代码,v-if
将分别重复运行于每个 v-for
循环中:
<!-- 不推荐 -->
<ul><li v-for="user in users" v-if="user.active" :key="user.id">{{ user.name }}</li>
</ul>
两个建议的替代方案:
- 使用计算属性过滤:
<!-- 推荐 -->
<ul><li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>
</ul>
computed: {activeUsers() {return this.users.filter(user => user.active)}
}
- 使用
<template>
和v-for
把v-if
移到外层:
<!-- 推荐 -->
<ul><template v-for="user in users" :key="user.id"><li v-if="user.active">{{ user.name }}</li></template>
</ul>
10. 列表渲染
10.1 v-for 基础用法
10.1.1 遍历数组
使用 v-for
指令基于一个数组来渲染一个列表:
<ul><li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>
也可以访问当前项的索引:
<ul><li v-for="(item, index) in items" :key="item.id">{{ index }} - {{ item.text }}</li>
</ul>
10.1.2 遍历对象
可以用 v-for
遍历对象的属性:
<ul><li v-for="value in object" :key="value">{{ value }}</li>
</ul>
可以提供第二个参数为键名:
<ul><li v-for="(value, key) in object" :key="key">{{ key }}: {{ value }}</li>
</ul>
还可以提供第三个参数为索引:
<ul><li v-for="(value, key, index) in object" :key="key">{{ index }}. {{ key }}: {{ value }}</li>
</ul>
10.1.3 遍历数字范围
v-for
也可以接受整数,会重复对应次数:
<div><span v-for="n in 10" :key="n">{{ n }} </span>
</div>
注意这里的 n
是从 1 开始,而不是从 0 开始。
10.2 维护状态与 key
Vue 默认使用"就地更新"的策略,如果数据项的顺序被改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是更新每个元素。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,需要为每项提供一个唯一的 key
属性:
<div v-for="item in items" :key="item.id"><!-- 内容 -->
</div>
key 的最佳实践:
- 使用唯一标识符(如 id)作为 key
- 不要使用索引作为 key(除非列表是静态的且不会重新排序)
- 尽量不要使用随机值作为 key,这会导致性能低下
10.3 数组更新检测
10.3.1 变更方法
Vue 能够检测响应式数组的变更方法,这些方法会触发视图更新:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
示例:
// 这些方法会触发视图更新
this.items.push({ id: 4, text: 'New Item' })
this.items.pop()
10.3.2 替换数组
有些方法不会改变原始数组,而是返回一个新数组,例如:
filter()
concat()
slice()
当使用这些方法时,可以用新数组替换旧数组:
// 替换整个数组(Vue 能够检测)
this.items = this.items.filter(item => item.text.match(/Foo/))
10.3.3 注意事项
由于 JavaScript 的限制,Vue 不能检测到数组的以下变动:
-
利用索引直接设置一个数组项:
// 这不会触发视图更新(Vue 2) this.items[index] = newValue
-
修改数组的长度:
// 这不会触发视图更新(Vue 2) this.items.length = newLength
解决方案(Vue 2):
// 使用 Vue.set
Vue.set(this.items, index, newValue)
// 或
this.$set(this.items, index, newValue)// 使用 splice
this.items.splice(index, 1, newValue)// 修改长度
this.items.splice(newLength)
在 Vue 3 中,使用 Proxy 解决了这些问题,直接修改索引或长度会触发更新。
10.4 显示过滤/排序后的结果
显示一个数组的过滤或排序副本,而不实际改变原始数据:
<ul><li v-for="n in evenNumbers" :key="n">{{ n }}</li>
</ul>
data() {return {numbers: [1, 2, 3, 4, 5]}
},
computed: {evenNumbers() {return this.numbers.filter(n => n % 2 === 0)}
}
对于复杂的情况,也可以使用方法:
<ul><li v-for="n in even(numbers)" :key="n">{{ n }}</li>
</ul>
data() {return {numbers: [1, 2, 3, 4, 5]}
},
methods: {even(numbe