Vue.js(通常简称为 Vue)是一个用于构建用户界面的渐进式 JavaScript 框架。所谓"渐进式",意味着 Vue 的设计是由浅入深的,你可以根据自己的需求选择使用它的一部分或全部功能。
Vue 最初由尤雨溪(Evan You)在 2014 年创建,其设计灵感部分来源于 Angular,但更轻量级且更加灵活。Vue 专注于视图层(View Layer),易于与其他库或现有项目集成。
Vue.js 的核心特性包括:
v-
开头的特殊属性,用于在 HTML 中实现各种功能。Vue.js 相比其他框架具有以下优势:
Vue.js 的主要版本演进:
Vue.js 提供了多种安装方式,可以根据不同的项目需求选择:
最简单的方式是通过 CDN 直接在 HTML 文件中引入 Vue.js:
Vue 2.x 的 CDN 链接:
对于更复杂的项目,推荐使用 npm 或 yarn 安装:
# npm
npm install vue
# yarn
yarn add vue
Vue CLI 是 Vue.js 的官方脚手架工具,用于快速搭建 Vue 项目:
# 安装 Vue CLI
npm install -g @vue/cli
# 创建一个新项目
vue create my-project
# 或者使用图形界面
vue ui
Vite 是一个现代化的构建工具,由 Vue 团队开发,特别适合 Vue 项目:
# npm
npm create vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
一个典型的 Vue CLI 创建的项目结构如下:
my-project/
├── node_modules/ # 依赖包
├── public/ # 静态资源,不会被 webpack 处理
│ ├── favicon.ico # 网站图标
│ └── index.html # 页面模板
├── src/ # 源代码目录
│ ├── assets/ # 资源文件,会被 webpack 处理
│ ├── components/ # Vue 组件
│ ├── views/ # 页面级别的组件
│ ├── router/ # Vue Router 配置
│ ├── store/ # Vuex 状态管理
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── .gitignore # Git 忽略文件
├── babel.config.js # Babel 配置
├── package.json # 项目依赖和脚本
├── README.md # 项目说明
└── vue.config.js # Vue CLI 配置文件
Vite 创建的项目结构稍有不同:
my-vue-app/
├── node_modules/ # 依赖包
├── public/ # 静态资源
├── src/ # 源代码目录
│ ├── assets/ # 资源文件
│ ├── components/ # Vue 组件
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── .gitignore # Git 忽略文件
├── index.html # HTML 入口文件(注意位置不同)
├── package.json # 项目依赖和脚本
├── README.md # 项目说明
└── vite.config.js # Vite 配置文件
为了提高 Vue.js 开发效率,推荐使用以下工具:
创建一个简单的 Vue 应用,了解基本概念:
My First Vue App
{
{ message }}
创建项目后,修改 App.vue
文件:
{
{ message }}
在 Vue 2 中,每个 Vue 应用都是通过创建一个 Vue
实例开始的:
const vm = new Vue({
// 选项
})
在 Vue 3 中,我们使用 createApp
函数创建应用实例:
const app = Vue.createApp({
// 选项
})
// 挂载到 DOM 元素
app.mount('#app')
Vue 实例接受许多选项,常用的包括:
Vue 实例暴露了一些有用的实例属性和方法,以 $
前缀标识:
const vm = new Vue({
data: {
message: 'Hello'
}
})
// 实例属性
console.log(vm.$data.message) // 'Hello'
console.log(vm.$el) // DOM 元素
// 实例方法
vm.$watch('message', function(newValue, oldValue) {
// 当 message 变化时调用
})
vm.$set(object, key, value) // 响应式地设置属性
vm.$delete(object, key) // 响应式地删除属性
Vue 组件实例有一个完整的生命周期,从创建到销毁,可以在特定阶段执行自定义代码。
创建阶段 挂载阶段 更新阶段 销毁阶段
┌─────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ │ │ │ │ │ │ │
│ beforeCreate │ │ beforeMount │ │ beforeUpdate │ │ beforeDestroy │
│ │ │ │ │ │ │ (Vue 2) │
│ ↓ │ │ ↓ │ │ ↓ │ │ or │
│ │ │ │ │ │ │ beforeUnmount │
│ created │ │ mounted │ │ updated │ │ (Vue 3) │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ ↓ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ destroyed │
│ │ │ │ │ │ │ (Vue 2) │
│ │ │ │ │ │ │ or │
│ │ │ │ │ │ │ unmounted │
│ │ │ │ │ │ │ (Vue 3) │
└─────────────────────┘ └───────────────────────┘ └───────────────────────┘ └───────────────────────┘
beforeCreate:
created:
beforeMount:
mounted:
beforeUpdate:
updated:
beforeDestroy(Vue 2)/ beforeUnmount(Vue 3):
destroyed(Vue 2)/ unmounted(Vue 3):
activated:
deactivated:
errorCaptured:
renderTracked(Vue 3):
renderTriggered(Vue 3):
export default {
data() {
return {
message: 'Hello Vue'
}
},
beforeCreate() {
console.log('beforeCreate: 实例初始化')
// 此时无法访问 data
console.log('this.message:', this.message) // undefined
},
created() {
console.log('created: 实例已创建')
// 可以访问 data
console.log('this.message:', this.message) // 'Hello Vue'
// 可以执行异步操作
this.fetchData()
},
beforeMount() {
console.log('beforeMount: 挂载前')
// DOM 尚未创建
console.log(this.$el) // 尚未挂载的元素
},
mounted() {
console.log('mounted: 已挂载')
// 可以访问 DOM
console.log(this.$el) // 实际 DOM 元素
// 适合执行需要访问 DOM 的操作
this.setupChart()
},
beforeUpdate() {
console.log('beforeUpdate: 数据更新前')
// 可以在更新前访问现有的 DOM
},
updated() {
console.log('updated: 数据更新后')
// DOM 已更新
},
beforeUnmount() { // Vue 3 使用 beforeUnmount,Vue 2 使用 beforeDestroy
console.log('beforeUnmount: 卸载前')
// 清理资源
this.cleanupResources()
},
unmounted() { // Vue 3 使用 unmounted,Vue 2 使用 destroyed
console.log('unmounted: 已卸载')
// 实例已销毁
},
// 辅助方法
methods: {
fetchData() {
// 获取初始数据
},
setupChart() {
// 设置图表
},
cleanupResources() {
// 清理资源
}
}
}
Vue 使用基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定到底层组件实例的数据。
最基本的数据绑定形式是使用"Mustache"语法(双大括号):
消息:{
{ message }}
{ { message }}
会被替换为对应数据对象上 message
属性的值。每当 message
属性变化时,插值内容也会更新。
双大括号会将数据解释为普通文本,如果要输出 HTML,需要使用 v-html
指令:
使用文本插值:{
{ rawHtml }}
使用 v-html 指令:
注意:在网站上动态渲染任意 HTML 可能会导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。
Mustache 语法不能用在 HTML 属性上,应该使用 v-bind
指令:
简写形式(推荐使用):
对于布尔属性,如果条件是 truthy 值,则属性会被包含;否则会被省略:
Vue 支持在绑定表达式中使用完整的 JavaScript 表达式:
{
{ number + 1 }}
{
{ ok ? 'YES' : 'NO' }}
{
{ message.split('').reverse().join('') }}
每个绑定只能包含单个表达式,以下内容不会生效:
{
{ var a = 1 }}
{
{ if (ok) { return message } }}
指令是带有 v-
前缀的特殊属性,用于在表达式的值改变时响应式地作用于 DOM。
现在你看到我了
当 seen
的值为 true 时, 元素会被渲染;否则会被移除。
一些指令可以接收一个"参数",在指令名后以冒号表示:
链接
点击
可以使用方括号括起来的 JavaScript 表达式作为指令的参数:
链接
点击
这里的 attributeName
会被作为一个 JavaScript 表达式进行动态求值,求值结果作为最终的参数使用。
修饰符是以 .
开头的特殊后缀,用于指定指令应该以特殊方式绑定:
这里 .prevent
修饰符表示调用 event.preventDefault()
方法,阻止表单的默认提交行为。
Vue 提供了许多内置指令,以下是一些最常用的:
绑定 HTML 属性或组件 prop 到表达式:
绑定事件监听器:
创建双向数据绑定:
条件性渲染元素:
A
B
Not A/B
根据条件显示/隐藏元素(元素始终会被渲染):
内容
基于数组/对象渲染列表:
- {
{ item.text }}
- {
{ index }}: {
{ item.text }}
{
{ index }}. {
{ key }}: {
{ value }}
只渲染元素和组件一次:
这个将不会改变: {
{ message }}
更新元素的 innerHTML:
更新元素的文本内容:
{
{ message }}
用于隐藏尚未编译的 Mustache 标签,直到组件实例准备完毕:
{
{ message }}
需要配合 CSS 规则使用:
[v-cloak] {
display: none;
}
跳过这个元素和它的子元素的编译过程:
{
{ 这里的内容不会被编译 }}
Vue 的响应式系统是其核心特性之一,使得数据和视图之间保持同步。
Vue 2 使用 Object.defineProperty
劫持对象属性的 getter 和 setter,在访问或修改属性时进行依赖收集和通知更新。
基本流程:
data
选项。Object.defineProperty
把这些属性全部转为 getter/setter。限制:
Vue 3 使用 ES6 的 Proxy
代替 Object.defineProperty
,可以监听整个对象而不是个别属性,解决了 Vue 2 中的限制。
基本流程:
reactive
函数。Proxy
包装该对象,拦截对该对象的所有操作。优势:
计算属性用于声明依赖于其他属性的复杂逻辑。计算属性是基于它们的依赖进行缓存的,只有在依赖变化时才会重新计算。
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
}
在模板中使用:
{
{ fullName }}
计算属性会基于其依赖进行缓存,只有在依赖发生变化时才会重新计算。而方法在每次重新渲染时都会执行。
// 计算属性
computed: {
expensiveComputation() {
console.log('计算属性被执行')
return this.items.filter(item => item.active).map(item => item.value)
}
}
// 方法
methods: {
expensiveMethod() {
console.log('方法被执行')
return this.items.filter(item => item.active).map(item => item.value)
}
}
当多次访问 expensiveComputation
时,如果依赖没有变化,计算结果会从缓存中返回,而不会重新执行过滤和映射操作。而每次访问 expensiveMethod()
都会导致函数重新执行。
计算属性默认只有 getter,但我们也可以提供 setter:
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
现在当我们运行 this.fullName = 'Jane Smith'
时,setter 会被调用,this.firstName
和 this.lastName
会被相应地更新。
在 Vue 3 的 Composition API 中,计算属性通过 computed
函数创建:
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
// 带有 setter 的计算属性
const fullNameWithSetter = computed({
get: () => firstName.value + ' ' + lastName.value,
set: (newValue) => {
const names = newValue.split(' ')
firstName.value = names[0]
lastName.value = names[names.length - 1]
}
})
return {
firstName,
lastName,
fullName,
fullNameWithSetter
}
}
}
侦听器(watch)用于在数据变化时执行异步或开销较大的操作。
export default {
data() {
return {
question: '',
answer: '问题包含问号(?)才能得到答案。'
}
},
watch: {
// 监听 question 变化
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
}
},
methods: {
async getAnswer() {
this.answer = '思考中...'
try {
const response = await fetch('https://yesno.wtf/api')
const data = await response.json()
this.answer = data.answer
} catch (error) {
this.answer = '错误!无法获取答案。'
}
}
}
}
对于复杂数据类型(对象、数组),默认只检测引用变化,要检测内部变化需要使用深度侦听:
watch: {
// 对象深度监听
userProfile: {
handler(newValue, oldValue) {
console.log('用户资料变化了')
},
deep: true
},
// 监听对象的特定属性
'userProfile.name'(newValue, oldValue) {
console.log('用户名变化了')
}
}
侦听器支持以下选项:
'pre'
、'post'
或 'sync'
)。watch: {
searchQuery: {
handler(newQuery, oldQuery) {
// 执行搜索
this.fetchResults(newQuery)
},
// 立即执行一次
immediate: true,
// 在组件更新后调用
flush: 'post' // Vue 3 特有
}
}
除了在组件选项中定义侦听器外,还可以使用实例方法 $watch
命令式地创建侦听器:
// 创建侦听器
const unwatch = this.$watch('question', function(newQuestion, oldQuestion) {
// 执行操作
if (newQuestion.includes('?')) {
this.getAnswer()
}
}, {
immediate: true,
deep: true
})
// 停止侦听
unwatch() // 当不再需要时
在 Vue 3 的 Composition API 中,侦听器通过 watch
和 watchEffect
函数创建:
import { ref, watch, watchEffect } from 'vue'
export default {
setup() {
const question = ref('')
const answer = ref('问题包含问号(?)才能得到答案。')
// 基本侦听
watch(question, (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
getAnswer()
}
})
// 侦听多个来源
const firstName = ref('John')
const lastName = ref('Doe')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('名字变化了')
})
// 深度侦听
const userProfile = ref({
name: 'John',
age: 30
})
watch(userProfile, (newProfile, oldProfile) => {
console.log('用户资料变化了')
}, { deep: true })
// watchEffect 会自动跟踪所有依赖,并在依赖变化时重新执行
watchEffect(() => {
console.log(`问题是: ${question.value}`)
console.log(`回答是: ${answer.value}`)
})
async function getAnswer() {
answer.value = '思考中...'
try {
const response = await fetch('https://yesno.wtf/api')
const data = await response.json()
answer.value = data.answer
} catch (error) {
answer.value = '错误!无法获取答案。'
}
}
return {
question,
answer
}
}
}
虽然计算属性和侦听器在很多情况下可以互换使用,但它们有不同的用途:
选择合适工具的基本原则:
通过传递对象给 :class
来动态切换 class:
上面的语法表示 active
这个 class 存在与否将取决于数据属性 isActive
的真假值,text-danger
同理。
也可以绑定一个返回对象的计算属性:
data() {
return {
isActive: true,
error: null
}
},
computed: {
classObject() {
return {