一、Vue 实例与数据绑定详解
1.1 Vue 实例是什么?
Vue 实例是 Vue 应用的入口点,每个 Vue 应用都是通过创建 Vue 实例开始的。
// Vue 实例创建语法
const vm = new Vue({
// 配置选项
el: '#app', // 挂载点
data: {}, // 数据
methods: {}, // 方法
computed: {}, // 计算属性
watch: {}, // 侦听器
// ... 其他选项
})
1.2 el 选项的两种写法
写法一:创建时直接指定
// 创建实例时指定挂载点
const vm = new Vue({
el: '#app', // 可以是CSS选择器字符串
data: { message: 'Hello' }
})
// 或者传入DOM元素
const appElement = document.getElementById('app')
const vm = new Vue({
el: appElement, // 也可以是DOM元素
data: { message: 'Hello' }
})
写法二:创建后手动挂载
// 1. 先创建实例(不指定el)
const vm = new Vue({
data: { message: 'Hello' }
})
// 2. 稍后手动挂载
setTimeout(() => {
vm.$mount('#app') // 方式一:使用选择器
// 或
const element = document.getElementById('app')
vm.$mount(element) // 方式二:使用DOM元素
}, 1000)
何时使用$mount?
- 需要条件挂载:根据用户权限、设备类型等决定挂载点
- 异步加载场景:等待某些条件满足后再挂载
- 测试环境:更方便地控制挂载时机
1.3 data 的两种写法详解
写法一:对象式(Vue实例使用)
// 适用于根实例或简单场景
new Vue({
el: '#app',
data: {
message: 'Hello Vue',
count: 0,
user: {
name: '张三',
age: 25
}
}
})
写法二:函数式(组件必须使用)
// 适用于组件(Vue.component 或 .vue 文件)
Vue.component('my-component', {
data() {
return {
message: 'Hello',
count: 0
}
}
})
// 或单文件组件中
export default {
data() {
return {
message: 'Hello',
count: 0
}
}
}
为什么组件必须用函数式?
// 错误示例:对象式data会导致数据共享问题
const CounterComponent = {
data: { count: 0 }, // 所有实例共享同一个对象
template: '<button @click="count++">{{ count }}</button>'
}
// 使用组件
<counter-component></counter-component>
<counter-component></counter-component>
// 两个组件会显示相同的count值,点击一个另一个也会变
// 正确示例:函数式保证每个实例有自己的数据
const CounterComponent = {
data() {
return { count: 0 } // 每次返回新对象
},
template: '<button @click="count++">{{ count }}</button>'
}
// 现在每个组件实例都有独立的count
二、模板语法深度解析
2.1 插值表达式 {{}} 的完整用法
基本语法:
<div id="app">
<!-- 显示纯文本 -->
<p>{{ message }}</p>
<!-- 支持JavaScript表达式 -->
<p>{{ message + '!' }}</p>
<p>{{ count + 1 }}</p>
<p>{{ price * quantity }}</p>
<p>{{ score >= 60 ? '及格' : '不及格' }}</p>
<!-- 调用方法 -->
<p>{{ getFullName() }}</p>
<!-- 访问对象属性 -->
<p>{{ user.name }}</p>
<p>{{ user.address.city }}</p>
<!-- 数组操作 -->
<p>{{ list[0] }}</p>
<p>{{ list.slice(0, 3) }}</p>
<!-- 字符串操作 -->
<p>{{ text.toUpperCase() }}</p>
<p>{{ text.substring(0, 10) + '...' }}</p>
</div>
不允许的用法:
<!-- 不能是语句 -->
{{ var a = 1 }}
{{ if(true) { return 'yes' } }}
{{ for(let i=0; i<10; i++) { console.log(i) } }}
<!-- 不能是函数/类定义 -->
{{ function() { return 1 } }}
{{ class MyClass {} }}
<!-- 不能是赋值表达式 -->
{{ a = 1 }}
{{ a += 1 }}
<!-- 不能在标签属性中直接使用 -->
<div id="{{ id }}">错误</div> <!-- 应该用 v-bind -->
2.2 指令语法:v-bind 详细讲解
基础绑定:
<!-- 绑定属性 -->
<img v-bind:src="imageSrc">
<a v-bind:href="url">链接</a>
<input v-bind:value="inputValue">
<!-- 简写形式 -->
<img :src="imageSrc">
<a :href="url">链接</a>
<input :value="inputValue">
绑定class的多种方式:
<!-- 1. 对象语法(最常用) -->
<div :class="{ active: isActive, 'text-danger': hasError }">
对象语法
</div>
<!-- 渲染为:<div class="active text-danger"></div> -->
<!-- 2. 数组语法 -->
<div :class="[activeClass, errorClass]">
数组语法
</div>
<!-- 相当于:<div class="active text-danger"></div> -->
<!-- 3. 混合语法 -->
<div :class="['base-class', { active: isActive, disabled: isDisabled }]">
混合语法
</div>
<!-- 4. 计算属性返回class对象 -->
<div :class="classObject">
计算属性
</div>
<script>
new Vue({
data: {
isActive: true,
hasError: false,
activeClass: 'active',
errorClass: 'text-danger'
},
computed: {
classObject() {
return {
active: this.isActive && !this.hasError,
'text-danger': this.hasError && this.error.type === 'fatal'
}
}
}
})
</script>
绑定style的详细用法:
<!-- 1. 内联对象语法 -->
<div :style="{
color: activeColor,
fontSize: fontSize + 'px',
'font-weight': isBold ? 'bold' : 'normal'
}">
内联样式
</div>
<!-- 2. 绑定样式对象 -->
<div :style="styleObject">
样式对象
</div>
<script>
new Vue({
data: {
styleObject: {
color: 'red',
fontSize: '13px',
backgroundColor: '#f5f5f5'
}
}
})
</script>
<!-- 3. 数组语法(合并多个样式对象) -->
<div :style="[baseStyles, overridingStyles]">
数组语法
</div>
<!-- 4. 自动添加浏览器前缀 -->
<div :style="{ transform: 'rotate(45deg)' }">
自动加前缀
</div>
<!-- Vue会自动处理为:transform, -webkit-transform, -ms-transform等 -->
<!-- 5. 多重值(浏览器兼容) -->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }">
多重值
</div>
<!-- 浏览器会依次检查支持哪个值,使用第一个支持的 -->
2.3 v-model 双向绑定深度解析
基础用法:
<!-- 文本输入 -->
<input v-model="message" placeholder="编辑我...">
<p>输入的内容是:{{ message }}</p>
<!-- 多行文本 -->
<textarea v-model="message" placeholder="多行文本..."></textarea>
<!-- 复选框(单个) -->
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<!-- 复选框(多个,绑定到数组) -->
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<p>选中的名字:{{ checkedNames }}</p>
<!-- 单选按钮 -->
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<p>选中的是:{{ picked }}</p>
<!-- 选择框(单选) -->
<select v-model="selected">
<option disabled value="">请选择</option>
<option value="A">选项A</option>
<option value="B">选项B</option>
</select>
<p>选中的是:{{ selected }}</p>
<!-- 选择框(多选,绑定到数组) -->
<select v-model="multiSelected" multiple style="width: 50px;">
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>多选的结果:{{ multiSelected }}</p>
修饰符详解:
<!-- .lazy:输入完成后同步(失去焦点或按回车时) -->
<input v-model.lazy="msg">
<p>lazy值:{{ msg }}</p>
<!-- 普通v-model:每输入一个字符就同步一次 -->
<!-- lazy:输入完成后才同步 -->
<!-- .number:自动将输入转为数值类型 -->
<input v-model.number="age" type="number">
<p>age的类型:{{ typeof age }}</p>
<!-- 输入"25"会转为数字25,而不是字符串"25" -->
<!-- .trim:自动去除首尾空白字符 -->
<input v-model.trim="username">
<p>用户名:"{{ username }}"</p>
<!-- 输入" admin "会自动转为"admin" -->
<!-- 修饰符可以串联使用 -->
<input v-model.lazy.trim="searchText">
v-model 原理(自定义组件中实现):
Vue.component('custom-input', {
props: ['value'], // 接收父组件传递的值
template: `
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
`
})
// 使用
<custom-input v-model="message"></custom-input>
<!-- 相当于 -->
<custom-input
:value="message"
@input="message = $event"
></custom-input>
三、事件处理完整指南
3.1 事件绑定语法详解
基本绑定:
<!-- 方法处理器 -->
<button v-on:click="greet">打招呼</button>
<!-- 简写 -->
<button @click="greet">打招呼</button>
<!-- 内联语句 -->
<button @click="count += 1">增加:{{ count }}</button>
<!-- 调用方法 -->
<button @click="say('Hello')">说Hello</button>
<!-- 传递事件对象 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
提交
</button>
<!-- 访问原始DOM事件 -->
<input @input="onInput">
methods 配置详解:
new Vue({
data: {
count: 0,
name: 'Vue.js'
},
methods: {
// 基本方法
greet() {
alert('Hello ' + this.name + '!')
},
// 带参数的方法
say(message) {
alert(message)
},
// 使用事件对象
warn(message, event) {
// 阻止默认行为
if (event) {
event.preventDefault()
}
alert(message)
},
// 访问事件目标
onInput(event) {
console.log('输入的值:', event.target.value)
console.log('事件类型:', event.type)
console.log('触发元素:', event.target.tagName)
},
// 在方法中使用数据
increment() {
this.count++ // 可以访问实例数据
console.log('当前计数:', this.count)
}
}
})
3.2 事件修饰符完整用法
.prevent:阻止默认行为
<!-- 阻止链接跳转 -->
<a @click.prevent="doSomething">链接</a>
<!-- 阻止表单提交 -->
<form @submit.prevent="onSubmit">
<button type="submit">提交</button>
</form>
<!-- 阻止右键菜单 -->
<div @contextmenu.prevent>禁用右键</div>
.stop:阻止事件冒泡
<!-- 父元素也有click事件 -->
<div @click="parentClick">
<!-- 点击按钮不会触发父元素的click -->
<button @click.stop="childClick">按钮</button>
</div>
<!-- 实际应用:模态框关闭 -->
<div class="modal" @click="closeModal">
<div class="modal-content" @click.stop>
<!-- 点击内容区域不会关闭模态框 -->
模态框内容
</div>
</div>
其他修饰符:
<!-- .capture:使用捕获模式 -->
<!-- 事件从外向内捕获,先执行父元素事件 -->
<div @click.capture="parentClick">
<button @click="childClick">按钮</button>
</div>
<!-- 点击按钮:先执行parentClick,再执行childClick -->
<!-- .self:只有事件在元素自身触发时才调用 -->
<!-- 点击子元素不会触发 -->
<div @click.self="handleSelf">
<p>点击这个p不会触发div的click事件</p>
</div>
<!-- .once:事件只触发一次 -->
<button @click.once="init">初始化(只执行一次)</button>
<!-- .passive:提升滚动性能 -->
<!-- 告诉浏览器你不想阻止事件的默认行为 -->
<div @scroll.passive="onScroll">
滚动内容...
</div>
<!-- 特别适合移动端,提升滚动流畅度 -->
修饰符串联使用:
<!-- 顺序很重要:会按顺序执行 -->
<a @click.stop.prevent="doSomething">
阻止跳转和冒泡
</a>
<!-- 先执行.stop,再执行.prevent -->
<!-- 多个修饰符 -->
<form @submit.prevent.stop.once="handleSubmit">
只提交一次的表单
</form>
3.3 按键修饰符和系统修饰符
按键修饰符:
<!-- Vue提供的按键别名 -->
<input @keyup.enter="submit"> <!-- 回车 -->
<input @keyup.tab="nextField"> <!-- Tab -->
<input @keyup.delete="deleteItem"> <!-- 删除 -->
<input @keyup.esc="cancel"> <!-- ESC -->
<input @keyup.space="playPause"> <!-- 空格 -->
<input @keyup.up="moveUp"> <!-- 上箭头 -->
<input @keyup.down="moveDown"> <!-- 下箭头 -->
<input @keyup.left="moveLeft"> <!-- 左箭头 -->
<input @keyup.right="moveRight"> <!-- 右箭头 -->
<!-- 数字键 -->
<input @keyup.1="selectOption1"> <!-- 数字1 -->
<input @keyup.2="selectOption2"> <!-- 数字2 -->
<!-- 字母键 -->
<input @keyup.a="pressA"> <!-- A键 -->
<input @keyup.b="pressB"> <!-- B键 -->
<!-- 使用键码(不推荐,因为键码可能变动) -->
<input @keyup.13="submit"> <!-- 13是回车键码 -->
<!-- 自定义按键别名 -->
<script>
Vue.config.keyCodes = {
f1: 112,
f2: 113,
custom: 86, // v键
pageUp: 33,
pageDown: 34
}
</script>
<input @keyup.f1="showHelp"> <!-- F1键 -->
<input @keyup.custom="doCustom"> <!-- 自定义键 -->
系统修饰键:
<!-- Ctrl + 点击 -->
<button @click.ctrl="doSomething">Ctrl+点击</button>
<!-- Alt + 点击 -->
<button @click.alt="doSomething">Alt+点击</button>
<!-- Shift + 点击 -->
<button @click.shift="doSomething">Shift+点击</button>
<!-- Meta(Windows键或Command键) -->
<button @click.meta="doSomething">Meta+点击</button>
<!-- 精确修饰符 .exact -->
<!-- 只有Ctrl被按下时才触发 -->
<button @click.ctrl.exact="ctrlOnly">仅Ctrl</button>
<!-- 没有任何系统修饰符被按下时才触发 -->
<button @click.exact="noModifiers">无修饰键</button>
鼠标按钮修饰符:
<!-- 左键点击 -->
<button @click.left="leftClick">左键</button>
<!-- 右键点击 -->
<button @click.right="rightClick">右键</button>
<!-- 中键点击 -->
<button @click.middle="middleClick">中键</button>
四、计算属性与侦听器深度解析
4.1 计算属性(Computed)完整指南
计算属性的特点:
-
缓存机制:依赖不变时不重新计算
-
响应式:依赖的响应式数据变化时自动更新
-
声明式:像普通属性一样使用,不用关心如何计算
基本用法:
new Vue({
data: {
firstName: '张',
lastName: '三',
price: 100,
quantity: 5,
discount: 0.1
},
computed: {
// 1. 简写形式(只有getter)
fullName() {
return this.firstName + this.lastName
},
// 2. 完整形式(包含getter和setter)
fullNameWithSetter: {
get() {
return this.firstName + ' ' + this.lastName
},
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
},
// 3. 依赖多个数据
totalPrice() {
return this.price * this.quantity * (1 - this.discount)
},
// 4. 格式化数据
formattedPrice() {
return '¥' + this.totalPrice.toFixed(2)
},
// 5. 过滤列表
activeUsers() {
return this.users.filter(user => user.isActive)
},
// 6. 排序列表
sortedItems() {
return [...this.items].sort((a, b) => a.price - b.price)
}
}
})
在模板中使用计算属性:
<div id="app">
<!-- 像普通属性一样使用 -->
<p>全名:{{ fullName }}</p>
<p>总价:{{ formattedPrice }}</p>
<!-- 双向绑定计算属性(需要有setter) -->
<input v-model="fullNameWithSetter">
<!-- 在v-for中使用 -->
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
<!-- 在样式绑定中使用 -->
<div :class="{ 'high-price': totalPrice > 1000 }">
价格:{{ formattedPrice }}
</div>
<!-- 在条件渲染中使用 -->
<div v-if="hasActiveUsers">
有活跃用户
</div>
</div>
计算属性的缓存机制:
computed: {
// 这个计算属性会被缓存
expensiveCalculation() {
console.log('重新计算...')
// 假设这是一个耗时的计算
let result = 0
for (let i = 0; i < 1000000; i++) {
result += Math.sqrt(i)
}
return result
}
}
// 多次访问expensiveCalculation,只计算一次
console.log(vm.expensiveCalculation) // 输出"重新计算...",然后返回结果
console.log(vm.expensiveCalculation) // 直接返回缓存结果,不输出
vm.someDependency = 'new value' // 依赖变化
console.log(vm.expensiveCalculation) // 再次输出"重新计算..."
4.2 侦听器(Watch)完整指南
侦听器的特点:
-
观察数据变化:响应特定数据的变化
-
执行副作用:适合执行异步操作或复杂逻辑
-
无缓存:每次变化都会执行
基本语法:
new Vue({
data: {
message: 'Hello',
user: {
name: '张三',
age: 25,
address: {
city: '北京',
street: '朝阳路'
}
},
searchText: '',
formData: {
title: '',
content: ''
}
},
watch: {
// 1. 基本侦听(函数形式)
message(newVal, oldVal) {
console.log(`消息从 "${oldVal}" 变为 "${newVal}"`)
},
// 2. 深度侦听对象(对象形式)
user: {
deep: true, // 深度侦听对象内部变化
immediate: true, // 立即执行一次handler
handler(newVal, oldVal) {
console.log('用户信息发生变化')
// 保存到本地存储
localStorage.setItem('user', JSON.stringify(newVal))
}
},
// 3. 侦听对象属性(字符串形式)
'user.name': function(newVal, oldVal) {
console.log(`用户名从 ${oldVal} 改为 ${newVal}`)
},
// 4. 侦听嵌套属性
'user.address.city': function(newCity) {
console.log('城市改为:', newCity)
},
// 5. 侦听计算属性
fullName(newVal) {
console.log('全名更新为:', newVal)
}
}
})
实际应用场景:
场景一:搜索功能防抖
watch: {
searchText: {
handler(newVal) {
// 清除之前的定时器
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
// 设置新的定时器
this.searchTimer = setTimeout(() => {
this.performSearch(newVal)
}, 500) // 500毫秒防抖
},
immediate: true // 组件创建时立即执行
}
},
methods: {
async performSearch(keyword) {
if (!keyword.trim()) {
this.searchResults = []
return
}
try {
this.isLoading = true
const response = await fetch(`/api/search?q=${keyword}`)
this.searchResults = await response.json()
} catch (error) {
console.error('搜索失败:', error)
} finally {
this.isLoading = false
}
}
}
场景二:表单验证
watch: {
email: {
handler(newVal) {
this.validateEmail(newVal)
},
immediate: true
},
password: {
handler(newVal) {
this.validatePassword(newVal)
},
immediate: true
}
},
methods: {
validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email) {
this.emailError = '邮箱不能为空'
} else if (!emailRegex.test(email)) {
this.emailError = '邮箱格式不正确'
} else {
this.emailError = ''
}
},
validatePassword(password) {
if (!password) {
this.passwordError = '密码不能为空'
} else if (password.length < 6) {
this.passwordError = '密码至少6位'
} else {
this.passwordError = ''
}
}
}
场景三:路由参数变化
watch: {
// 侦听路由参数变化
'$route.params.id': {
handler(newId) {
this.loadProductDetails(newId)
},
immediate: true
},
// 侦听查询参数变化
'$route.query': {
handler(newQuery) {
this.applyFilters(newQuery)
},
deep: true
}
},
methods: {
async loadProductDetails(productId) {
try {
this.isLoading = true
const response = await fetch(`/api/products/${productId}`)
this.product = await response.json()
} catch (error) {
console.error('加载失败:', error)
this.error = '加载产品信息失败'
} finally {
this.isLoading = false
}
},
applyFilters(query) {
// 根据查询参数过滤数据
this.filters = { ...this.filters, ...query }
this.filterProducts()
}
}
API方式使用侦听器:
const vm = new Vue({
data: { count: 0 }
})
// 使用$watch API
const unwatch = vm.$watch('count', function(newVal, oldVal) {
console.log(`count从${oldVal}变为${newVal}`)
}, {
deep: false,
immediate: false
})
// 修改数据,触发侦听器
vm.count = 1 // 输出:count从0变为1
// 停止侦听
unwatch()
vm.count = 2 // 不会触发侦听器
4.3 Computed vs Watch 选择指南
何时使用计算属性?
// 场景1:需要基于现有数据计算新值
computed: {
// 全名 = 姓 + 名
fullName() {
return this.firstName + ' ' + this.lastName
},
// 过滤并排序列表
sortedActiveUsers() {
return this.users
.filter(user => user.isActive)
.sort((a, b) => a.name.localeCompare(b.name))
},
// 格式化显示
formattedDate() {
return new Date(this.timestamp).toLocaleDateString()
}
}
何时使用侦听器?
// 场景1:需要执行异步操作
watch: {
searchText(newVal) {
// 发送API请求
this.fetchSearchResults(newVal)
}
},
// 场景2:数据变化需要执行多个操作
watch: {
formData: {
deep: true,
handler(newVal) {
// 1. 保存草稿
this.saveDraft(newVal)
// 2. 验证表单
this.validateForm(newVal)
// 3. 发送分析事件
this.sendAnalytics('form_changed', newVal)
}
}
},
// 场景3:需要访问新旧值
watch: {
price(newPrice, oldPrice) {
const change = ((newPrice - oldPrice) / oldPrice * 100).toFixed(2)
console.log(`价格变化:${change}%`)
}
}
性能优化建议:
-
能用计算属性就不用侦听器:计算属性有缓存,性能更好
-
避免在计算属性中执行异步操作:计算属性应该是同步的
-
深度侦听要谨慎:
deep: true 会遍历对象所有属性,性能开销大
-
及时清理定时器:在beforeDestroy中清理侦听器中的定时器
beforeDestroy() {
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
if (this.unwatch) {
this.unwatch()
}
}
5.条件渲染
5.1 v-if 指令
语法:
<div v-if="表达式">内容</div>
<div v-else-if="表达式">内容</div>
<div v-else>内容</div>
特点:
- 基于条件判断是否创建或移除DOM元素
- 切换频率较低的场景使用
- v-if、v-else-if、v-else必须连续使用,中间不能被打断
示例:
<div id="root">
<div v-if="type === 'A'">显示A</div>
<div v-else-if="type === 'B'">显示B</div>
<div v-else>显示其他</div>
</div>
5.2 v-show 指令
语法:
<div v-show="表达式">内容</div>
特点:
- 通过CSS的
display: none控制显示/隐藏
- DOM元素始终存在,只是视觉上隐藏
- 切换频率高的场景使用
- 初始渲染开销大,切换开销小
5.3 v-if vs v-show 对比
| 特性 |
v-if |
v-show |
| DOM操作 |
添加/删除DOM节点 |
切换CSS的display属性 |
| 初始渲染 |
条件为false时不渲染 |
无论条件都渲染,然后隐藏 |
| 切换开销 |
高(需要重建DOM) |
低(只修改CSS) |
| 适用场景 |
不频繁切换的场景 |
频繁切换的场景 |
| 性能影响 |
减少初始DOM节点数量 |
增加初始DOM节点数量 |
5.4 template 标签的使用
<template v-if="n === 1">
<h1>标题1</h1>
<p>段落1</p>
<span>内容1</span>
</template>
特点:
- 作为不可见的包裹元素,不会在最终HTML中显示
- 只能与
v-if、v-else-if、v-else、v-for配合使用
-
不能与
v-show一起使用
6、列表渲染
6.1 v-for 指令基础
语法:
<li v-for="(item, index) in items" :key="item.id">
{{ item.name }} - 索引: {{ index }}
</li>
可遍历的数据结构:
遍历数组(最常用):
<ul>
<li v-for="(person, index) in persons" :key="person.id">
{{ index }} - {{ person.name }} ({{ person.age }})
</li>
</ul>
遍历对象:
<ul>
<li v-for="(value, key, index) in car" :key="key">
{{ index }} - {{ key }}: {{ value }}
</li>
</ul>
遍历字符串(很少用):
<span v-for="(char, index) in 'hello'" :key="index">
{{ char }}
</span>
遍历数字(很少用):
<span v-for="n in 5" :key="n">{{ n }}</span>
<!-- 输出:1 2 3 4 5 -->
6.2 :key 的重要性
为什么需要key?
key是Vue识别节点的标识,用于高效的DOM更新。
key的内部原理(虚拟DOM Diff算法)
数据更新流程:
数据变化 → 响应式系统检测 → 重新生成虚拟DOM → 新旧虚拟DOM差异对比 → 最小化更新真实DOM
Diff算法对比规则:
-
找到相同key:
- 内容未变 → 复用之前的真实DOM
- 内容变化 → 生成新真实DOM,替换旧DOM
-
未找到相同key:
key的选择策略
推荐使用:
<li v-for="person in persons" :key="person.id">
{{ person.name }}
</li>
谨慎使用index作为key:
<!-- 可能引发问题的场景 -->
<li v-for="(person, index) in persons" :key="index">
{{ person.name }}
</li>
index作为key的问题:
-
效率问题:逆序添加/删除时,产生不必要的真实DOM更新
-
数据错乱:包含输入类DOM时,可能产生错误的DOM更新
何时可以使用index:
- 仅用于展示的静态列表
- 没有逆序操作
- 没有输入类表单元素
6.3 综合应用
列表过滤与排序
<div id="app">
<!-- 搜索功能 -->
<input type="text" v-model="keyword" placeholder="搜索姓名">
<!-- 排序按钮 -->
<button @click="sortType = 1">年龄升序 ↑</button>
<button @click="sortType = 2">年龄降序 ↓</button>
<button @click="sortType = 0">原顺序</button>
<!-- 列表渲染 -->
<ul>
<li v-for="p in filteredPersons" :key="p.id">
{{ p.id }} - {{ p.name }} ({{ p.age }}岁)
</li>
</ul>
</div>
<script>
new Vue({
el: '#app',
data: {
keyword: '',
sortType: 0, // 0:原顺序, 1:升序, 2:降序
persons: [
{ id: '001', name: '张三', age: 25 },
{ id: '002', name: '李四', age: 22 },
{ id: '003', name: '王五', age: 28 },
{ id: '004', name: '赵六', age: 20 }
]
},
computed: {
filteredPersons() {
let arr = this.persons
// 1. 过滤(根据关键字)
if (this.keyword) {
arr = arr.filter(person =>
person.name.includes(this.keyword)
)
}
// 2. 排序
if (this.sortType === 1) {
arr = [...arr].sort((a, b) => a.age - b.age) // 升序
} else if (this.sortType === 2) {
arr = [...arr].sort((a, b) => b.age - a.age) // 降序
}
return arr
}
}
})
</script>

数组排序方法详解
sort()方法比较函数:
arr.sort((a, b) => {
// 返回值 < 0: a排在b前面
// 返回值 > 0: b排在a前面
// 返回值 = 0: 保持原顺序
})
// 数字排序口诀:前减后得升序,后减前得降序
numbers.sort((a, b) => a - b) // 升序
numbers.sort((a, b) => b - a) // 降序
条件渲染与列表渲染结合
<div id="app">
<!-- 根据数据是否为空显示不同内容 -->
<div v-if="items.length === 0">
<p>暂无数据</p>
<button @click="loadData">加载数据</button>
</div>
<template v-else>
<h3>数据列表 (共{{ items.length }}条)</h3>
<ul>
<li v-for="item in items" :key="item.id">
<span v-if="item.isNew" class="new-badge">NEW</span>
{{ item.title }}
<span v-show="item.hot">热</span>
</li>
</ul>
</template>
</div>
6.4实践与注意事项
性能优化
-
合理使用v-if和v-show
- 初始不需要显示 → 使用
v-if
- 需要频繁切换 → 使用
v-show
-
避免v-if和v-for同时用在同一个元素
<!-- 不推荐 -->
<li v-for="user in users" v-if="user.isActive">
{{ user.name }}
</li>
<!-- 推荐:使用计算属性过滤 -->
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
-
为v-for始终添加key
常见问题解决
问题1:列表更新视图不刷新
// 正确
this.items.push(newItem)
this.items.splice(index, 1, newItem)
Vue.set(this.items, index, newValue)
// 错误(不会触发视图更新)
this.items[index] = newValue
this.items.length = 0
问题2:嵌套循环的key
<div v-for="category in categories" :key="category.id">
<h3>{{ category.name }}</h3>
<div v-for="product in category.products"
:key="product.id">
{{ product.name }}
</div>
</div>
7. 响应式原理回顾
Vue的响应式系统工作流程:
- 数据被Object.defineProperty()代理
- 数据变化触发setter
- 通知所有依赖的Watcher
- Watcher调用更新函数
- 重新生成虚拟DOM
- 执行Diff算法对比新旧虚拟DOM
- 最小化更新真实DOM
虚拟DOM的优势:
- 减少直接操作DOM的次数
- 批量更新,提高性能
- 跨平台能力(可渲染到不同平台)
总结
| 特性 |
条件渲染 |
列表渲染 |
| 主要指令 |
v-if, v-else-if, v-else, v-show |
v-for |
| 核心概念 |
条件判断显示/隐藏 |
数据遍历 |
| 性能关键 |
合理选择v-if/v-show |
正确使用:key |
| 常用场景 |
模态框、选项卡、权限控制 |
数据列表、表格、菜单 |
key的正确使用:
<!-- 使用唯一标识 -->
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
<!-- 组合唯一键 -->
<li v-for="item in items" :key="`${item.category}-${item.id}`">
{{ item.name }}
</li>
<!-- 特殊情况:使用index(要谨慎) -->
<li v-for="(item, index) in staticList" :key="index">
{{ item }}
</li>
<!-- 仅在以下情况可以使用index:
1. 列表是静态的(不会重新排序、添加、删除)
2. 列表项没有表单元素
3. 列表项没有自己的状态 -->
数组更新检测:
// Vue能够检测到的数组变更方法:
// 1. push() - 末尾添加
// 2. pop() - 末尾删除
// 3. shift() - 开头删除
// 4. unshift() - 开头添加
// 5. splice() - 添加/删除
// 6. sort() - 排序
// 7. reverse() - 反转
// Vue能检测到变化:
vm.items.push({ id: 4, name: 'D' })
vm.items.splice(0, 1) // 删除第一个
vm.items.sort((a, b) => a.price - b.price)
// Vue检测不到的变化:
vm.items[0] = { id: 1, name: '新的A' } // 直接设置索引
vm.items.length = 0 // 直接修改length
// 解决方案:
Vue.set(vm.items, 0, { id: 1, name: '新的A' })
vm.items.splice(0, 1, { id: 1, name: '新的A' }) // 替换
vm.items = [] // 重新赋值
嵌套循环:
<!-- 外层循环 -->
<div v-for="category in categories" :key="category.id">
<h3>{{ category.name }}</h3>
<!-- 内层循环 -->
<ul>
<li v-for="product in category.products"
:key="product.id"
:class="{ 'new': product.isNew }">
{{ product.name }}
<span v-if="product.isHot">热</span>
</li>
</ul>
</div>
8.Vue数据代理
数据代理 是 Vue 中一个重要的机制,它允许开发者通过 this.属性名 直接访问 data 中的数据,而不需要写 this.data.属性名。
Vue 实例(this)代理了 data() 函数返回的对象中的所有属性
代理过程
第一步:原始数据存储
// 1. Vue 先执行你的 data() 函数,得到一个对象
const rawData = {
message: 'Hello Vue',
count: 0,
user: { name: 'John' }
};
// 2. Vue 把这个对象保存到实例的 _data 属性
this._data = rawData;
// 现在可以通过 this._data.message 访问
第二步:创建代理连接
// 3. Vue 在自身(this)上创建同名属性
// 对 message 属性的代理
Object.defineProperty(this, 'message', {
get() {
// 当有人访问 this.message 时
// 实际上返回的是 this._data.message
return this._data.message;
},
set(newValue) {
// 当有人设置 this.message = 'xxx' 时
// 实际上设置的是 this._data.message = 'xxx'
this._data.message = newValue;
}
});
// 对 count 做同样的代理
Object.defineProperty(this, 'count', {
get() { return this._data.count; },
set(val) { this._data.count = val; }
});
// 对 user 做同样的代理
Object.defineProperty(this, 'user', {
get() { return this._data.user; },
set(val) { this._data.user = val; }
});
第三步:使用时的效果
// 你写的代码
this.message = 'New Value';
// Vue 实际执行的操作
this._data.message = 'New Value';
// 这就是代理的核心:转发操作
实际工作流程:
- Vue 实例初始化时,将原始 data 保存到
_data 私有属性
- 遍历
_data 的所有属性名(如 'message', 'count')
- 为每个属性在 Vue 实例上定义同名属性
- 设置 getter:访问
this.message 时,实际返回 this._data.message
- 设置 setter:设置
this.message = 'new' 时,实际设置 this._data.message = 'new'
9.Vue数据监测
Vue 的核心特性之一就是数据驱动视图。当数据变化时,视图会自动更新。
Vue 2.x 的实现原理(基于 Object.defineProperty)
数据劫持
当你在 Vue 的 data 选项中定义数据时,Vue 会遍历这些数据的所有属性,并使用 JavaScript 的 Object.defineProperty 方法对每个属性进行"劫持"。
每个属性都被 Vue 安装了一个"监听器"。这个监听器有两项主要功能:
-
getter(获取拦截器):当你读取这个属性时,Vue 会记录下来"谁读取了我"
-
setter(设置拦截器):当你修改这个属性时,Vue 会通知所有依赖这个属性的地方进行更新
依赖收集
当一个组件或计算属性使用某个数据时,Vue 会创建一个"观察者"(Watcher)。在数据被读取的过程中,这个观察者会把自己"注册"到该数据的依赖列表中。
就好比你订阅了一份杂志:
- 当你第一次读取数据时,相当于你告诉杂志社:"我对这个数据感兴趣,请把我加入订阅名单"
- 数据变化时,杂志社(Vue)会给所有订阅者(Watcher)发送通知
发布-订阅模式
当数据发生变化时,setter 会被触发。这时 Vue 会:
- 检查新值是否与旧值相同(避免不必要的更新)
- 如果值确实改变了,通知所有依赖这个数据的观察者
- 观察者收到通知后,执行更新操作(比如重新渲染组件)
递归观测
如果数据的属性值是对象或数组,Vue 会递归地对它们进行同样的观测处理。这就是为什么嵌套对象和数组也能实现响应式。
Vue 2 响应式的局限性
Vue 2 使用 Object.defineProperty 实现响应式,这带来了一个关键限制:
data() {
return {
user: {
name: 'John',
age: 30
},
list: ['a', 'b', 'c']
};
},
created() {
// 问题1:添加新属性不会触发更新
this.user.email = 'john@example.com'; // 视图不会更新
// 问题2:通过索引设置数组项不会触发更新
this.list[0] = 'new value'; // 视图不会更新
// 问题3:修改数组长度不会触发更新
this.list.length = 0; // 视图不会更新
}
根本原因:
- Vue 2 只能在初始化时对已有的属性进行响应式处理
- 新增的属性没有经过
Object.defineProperty 处理
- 数组的索引和长度修改也无法被劫持
Vue 2.x 的方法有几个局限性:
- 无法检测到对象新属性的添加或删除 (需要通过vue.set)
- 对数组的某些操作不敏感(比如通过索引直接修改)
- 需要对每个属性单独劫持,性能开销较大
数组的特殊处理
由于历史原因和浏览器兼容性,Vue 对数组进行了特殊处理:
在 Vue 2.x 中:
数组索引不能用 Object.defineProperty
技术限制
const arr = ['a', 'b', 'c'];
// 理论上可以对索引使用 defineProperty
Object.defineProperty(arr, '0', {
get() { /* ... */ },
set() { /* ... */ }
});
// 但问题来了:
arr[100] = 'new'; // 创建了第100个元素
// 难道要对 0-100 每个索引都预先 defineProperty 吗
根本问题:
-
数组长度动态变化:无法预知会有多少个索引
-
性能不可行:如果数组有10000个元素,难道要定义10000个 getter/setter?
-
内存浪费:大多数索引可能永远不会被访问
数组访问的实际情况
场景分析
export default {
data() {
return {
list: ['a', 'b', 'c'],
users: [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
]
};
},
created() {
// 1. 访问数组本身 - 有 getter
console.log(this.list); // 触发 list 的 getter
// 2. 访问数组索引 - 没有 getter
console.log(this.list[0]); // 直接访问数组索引,无 getter
// 3. 访问数组中的对象属性 - 有 getter
console.log(this.users[0].name);
// 流程:
// - users[0]:无 getter(直接数组访问)
// - .name:有 getter(对象属性有响应式)
// 4. 通过索引修改 - 问题所在
this.list[0] = 'x'; // 不会触发更新
// 5. 使用方法修改 - 有效
this.list.push('d'); // 触发更新(重写的方法)
}
};
数组索引的特殊访问路径
虽然索引没有 getter/setter,但 Vue 通过另一种方式"感知"到数组元素的访问:
// 当你在模板中使用数组索引时:
<template>
<div>{{ list[0] }}</div>
<div>{{ users[0].name }}</div>
</template>
// Vue 的编译过程:
1. 编译模板时发现 list[0]
2. 创建对应的 Watcher(观察者)
3. 这个 Watcher 会:
- 读取 list(触发 list 的 getter,收集依赖)
- 然后读取 list[0](直接数组访问)
// 关键点:依赖是收集在「数组本身」上,而不是「数组索引」上
vue2能被检测到的数组操作
// 1. 使用重写的7个方法
this.list.push('new'); //
this.list.pop(); //
this.list.shift(); //
this.list.unshift('new'); //
this.list.splice(0, 1, 'x'); //
this.list.sort(); //
this.list.reverse(); //
// 2. 替换整个数组
this.list = ['new', 'array']; //
// 3. 使用 Vue.set(内部用 splice)
Vue.set(this.list, 0, 'new'); //
// 4. 修改 length(但有限制)
this.list.splice(newLength); // 通过 splice 修改长度
数组操作规范
// 正确:使用变异方法或 $set
this.list.push('new'); // 添加元素
this.list.splice(index, 1); // 删除元素
this.$set(this.list, index, 'new'); // 修改元素
// 避免:直接操作索引
this.list[index] = 'new'; // 不会触发更新
this.list.length = 0; // 不会触发更新
解决方案:Vue.set / this.$set
Vue 提供了 Vue.set(全局API)和 this.$set(实例方法)来解决这个问题:
// 正确做法
this.$set(this.user, 'email', 'john@example.com'); // 视图会更新
this.$set(this.list, 0, 'new value'); // 视图会更新
Vue.set 和 this.$set 的关系
它们是同一个函数
// Vue 源码中的实现
Vue.set = function (obj, key, value) {
// ... 实现逻辑
};
// 在组件实例上暴露为 $set
Vue.prototype.$set = Vue.set;
// 也就是说:
Vue.set === Vue.prototype.$set // true
使用场景区别
// 1. 在 Vue 组件内部:使用 this.$set(推荐)
export default {
methods: {
addProperty() {
this.$set(this.user, 'email', 'test@example.com');
}
}
};
// 2. 在 Vue 组件外部:使用 Vue.set
import Vue from 'vue';
const app = new Vue({ /* ... */ });
Vue.set(app.user, 'email', 'test@example.com');
// 3. 在非 Vue 上下文中:使用 Vue.set
const plainObject = { name: 'John' };
Vue.set(plainObject, 'age', 30); // 也可以用于普通对象
什么时候必须用 $set?
// 必须用 $set 的情况:
// 1. 给对象添加新属性
// 2. 通过索引修改数组项(当索引超出原长度或修改已有项)
// 不需要 $set 的情况:
// 1. 修改已有属性:this.user.name = 'new'
// 2. 使用数组变异方法:push, pop, splice 等
// 3. 替换整个数组:this.list = newList
10.Vue 内置指令
10.1 v-text 指令
作用:向所在节点渲染文本内容。
<div v-text="name">你好</div>
<div v-text="str"></div>
特点:
-
替换节点中的所有内容
-
不识别 HTML 结构(纯文本显示)
-
与 {{ }} 插值语法的区别:
-
v-text:完全替换内容
-
{{ }}:只替换占位符,保留其他内容
10.2 v-html 指令
作用:向指定节点渲染包含 HTML 结构的内容。
<div v-html="str"></div>
安全性警告:
11.自定义指令
11.1 定义语法
// 局部指令
new Vue({
directives: { 指令名: 配置对象 }
})
// 全局指令
Vue.directive('指令名', 配置对象)
11.2 三个核心钩子函数
Vue.directive('fbind', {
// 1. 指令与元素成功绑定时
bind(element, binding) {
element.value = binding.value
},
// 2. 元素被插入页面时
inserted(element, binding) {
element.focus()
},
// 3. 模板被重新解析时
update(element, binding) {
element.value = binding.value
}
})
11.3 参数详解
element:绑定的真实 DOM 元素。
binding 对象:
{
name: 'fbind', // 指令名(不带 v-)
value: 1, // 绑定的值
expression: 'n', // 表达式字符串
arg: 'value', // 参数(v-fbind:value 中的 value)
modifiers: {} // 修饰符对象
}
11.4 命名规范
-
指令定义时不加 v-,使用时要加 v-
-
多单词指令使用 kebab-case(如:v-big-number)
-
指令名要加引号的情况:命名中间有-
directives: {
'big-number': function(element, binding) {
// ...
}
}
12.Vue 生命周期
12.1 生命周期概念
12.2 完整的生命周期流程
创建阶段:
beforeCreate → created → beforeMount → mounted
更新阶段:
beforeUpdate → updated
销毁阶段:
beforeDestroy → destroyed
12.3 各阶段详解
(1) 创建阶段
-
beforeCreate:
- 实例刚创建,数据观测和事件配置还未开始
-
data、methods 等不可用
-
created:
- 实例创建完成,数据观测、属性和方法运算完成
-
data、methods 已可用,但 DOM 未生成
-
适合:发送异步请求、初始化数据
-
beforeMount:
- 模板编译完成,但未挂载到页面
- 虚拟 DOM 已创建
-
mounted:
- 实例挂载到 DOM 上,页面首次渲染完成
- 真实 DOM 已生成,可通过
this.$el 访问
(2) 更新阶段
-
beforeUpdate:
- 数据更新时调用,DOM 未重新渲染
- 可以获取更新前的 DOM 状态
-
updated:
- 数据更新导致 DOM 重新渲染后调用
- 避免在此阶段修改数据,可能导致无限循环
(3) 销毁阶段
-
beforeDestroy:
-
destroyed:
- 实例销毁后调用,所有绑定和监听被移除
- 子实例也被销毁

编辑
12.4 最常用的两个钩子详解
(1) mounted(挂载完成时)
调用时机:组件第一次显示在页面上之后。
应该做什么:
mounted() {
// 发送请求获取初始数据
this.fetchData()
// 启动定时器
this.timer = setInterval(() => {
// 轮询、倒计时等
}, 1000)
// 绑定自定义事件
this.$bus.$on('event', this.handleEvent)
// 操作 DOM(初始化第三方库)
this.initChart()
// 订阅消息
this.$store.subscribe(mutation => {
// 处理订阅
})
}
不应该做什么:
- 修改大量数据(可能导致频繁重新渲染)
- 执行耗时同步操作(会阻塞页面)
(2) beforeDestroy(销毁前)
调用时机:组件即将被销毁前。
必须做什么(防止内存泄漏):
beforeDestroy() {
// 清除定时器
clearInterval(this.timer)
clearTimeout(this.timeout)
// 解绑自定义事件
this.$bus.$off('event', this.handleEvent)
// 取消订阅
this.unsubscribe()
// 清理其他资源
this.websocket.close()
}
重要性:如果不清理,即使组件销毁了,这些资源还在后台运行,会造成内存泄漏
13.自定义指令中的 this 指向问题
13.1 问题现象
在自定义指令的钩子函数中,this 指向的是 window(全局对象),而不是 Vue 实例。
directives: {
big(element, binding) {
console.log(this) // window,不是 Vue 实例
console.log(this.message) // undefined,无法访问组件数据
}
}
13.2 原因分析
-
设计原因:保持指令的独立性和复用性
-
指令钩子是独立函数,不是 Vue 实例的方法
-
Vue 内部调用方式:
// 伪代码
function callHook(hookFn, el, binding, vnode) {
hookFn.call(window, el, binding, vnode) // 用 window 作为 this
}
13.3 解决方案
方案一:通过 binding.value 传递数据(推荐)
<div v-demo="dataValue"></div>
directives: {
demo: {
bind(el, binding) {
// 所有数据通过 binding.value 传递
el.innerText = binding.value
}
}
}
方案二:通过 vnode 访问 Vue 实例(不推荐,破坏封装性)
directives: {
demo: {
bind(el, binding, vnode) {
const vm = vnode.context // 获取 Vue 实例
console.log(vm.message) // 可以访问组件数据
}
}
}
13.4为什么这样设计?
-
保持指令独立:指令只依赖传入参数,不依赖外部状态
-
提高复用性:指令可在任何组件中使用
-
明确数据来源:所有数据通过
binding.value 显式传递,代码更清晰
对比其他地方的 this
| 位置 |
this 指向 |
示例 |
| methods 中的方法 |
Vue 实例 |
this.message 可以访问 |
| computed 计算属性 |
Vue 实例 |
this.count 可以访问 |
| watch 监听器 |
Vue 实例 |
this.$emit() 可以用 |
| 生命周期钩子 |
Vue 实例 |
this.$el 可以访问 |
| 自定义指令钩子 |
window |
this 没用 |
13.5常见问题
Q1:为什么组件的 data 必须是函数?
A:如果 data 是对象,所有组件实例会共享同一个 data 对象,修改一个实例的数据会影响所有实例。使用函数返回新对象可保证每个实例有独立的数据。
Q2:什么时候使用 v-text 而不是 {{ }}?
A:
- 当需要完全替换元素内容时用
v-text
- 当需要保留元素原有内容时用
{{ }}
- 当内容安全可信且需要显示 HTML 时用
v-html
Q3:生命周期函数可以异步吗?
A:可以,但需要注意:
-
created 中可以发送异步请求
-
mounted 中可以进行异步 DOM 操作
- 异步操作不会影响生命周期流程
Q4:为什么 beforeDestroy 中必须清理资源?
A:如果不清理:
- 定时器继续运行,占用内存
- 事件监听器未移除,可能触发错误
- 订阅未取消,可能产生内存泄漏
- 第三方库实例未销毁,可能冲突
Q5:如何选择使用局部指令还是全局指令?
A:
-
局部指令:只在当前组件或特定组件中使用
-
全局指令:在多个不相关的组件中都需要使用
Q6:自定义指令中如何传递复杂参数?
A:
<!-- 传递对象 -->
<div v-demo="{ color: 'red', size: 20 }"></div>
<!-- 传递多个参数 -->
<div v-demo:[arg].modifier="value"></div>
bind(el, binding) {
console.log(binding.value) // { color: 'red', size: 20 }
console.log(binding.arg) // 'arg'
console.log(binding.modifiers) // { modifier: true }
}
Q7:mounted 和 created 的区别是什么?
A: