本章将深入学习Vue.js的核心语法,包括模板语法、指令系统、事件处理和数据绑定等基础概念。
3.1 模板语法
文本插值
Vue.js使用双大括号(Mustache语法)进行文本插值。
<template>
<div>
<!-- 基本文本插值 -->
<p>{{ message }}</p>
<!-- 表达式插值 -->
<p>{{ number + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<!-- 方法调用 -->
<p>{{ formatMessage(message) }}</p>
<!-- 一次性插值 -->
<p v-once>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue.js!',
number: 42,
ok: true
}
},
methods: {
formatMessage(msg) {
return msg.toUpperCase()
}
}
}
</script>
# 运行示例
npm run dev
# 在浏览器中查看效果
HTML插值
<template>
<div>
<!-- 普通插值(会转义HTML) -->
<p>{{ rawHtml }}</p>
<!-- HTML插值(不会转义) -->
<p v-html="rawHtml"></p>
<!-- 安全的HTML插值 -->
<div v-html="sanitizedHtml"></div>
</div>
</template>
<script>
export default {
data() {
return {
rawHtml: '<span style="color: red">红色文本</span>'
}
},
computed: {
sanitizedHtml() {
// 在实际项目中,应该使用DOMPurify等库来清理HTML
return this.rawHtml.replace(/<script[^>]*>.*?<\/script>/gi, '')
}
}
}
</script>
属性绑定
<template>
<div>
<!-- 基本属性绑定 -->
<div v-bind:id="dynamicId">动态ID</div>
<div :id="dynamicId">简写语法</div>
<!-- 布尔属性 -->
<button :disabled="isButtonDisabled">按钮</button>
<!-- 多个属性 -->
<div v-bind="objectOfAttrs">多属性绑定</div>
<!-- 动态属性名 -->
<a :[attributeName]="url">动态属性</a>
<!-- 修饰符 -->
<div :text-content.prop="message">修饰符示例</div>
</div>
</template>
<script>
export default {
data() {
return {
dynamicId: 'my-id',
isButtonDisabled: false,
objectOfAttrs: {
id: 'container',
class: 'wrapper'
},
attributeName: 'href',
url: 'https://vuejs.org',
message: 'Hello World'
}
}
}
</script>
3.2 指令系统
v-if条件渲染
<template>
<div>
<!-- 基本条件渲染 -->
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
<!-- 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>
<!-- template包装 -->
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>
<!-- 切换按钮 -->
<button @click="toggleLoginType">切换登录类型</button>
</div>
</template>
<script>
export default {
data() {
return {
awesome: true,
type: 'A',
loginType: 'username'
}
},
methods: {
toggleLoginType() {
this.loginType = this.loginType === 'username' ? 'email' : 'username'
}
}
}
</script>
v-show显示隐藏
<template>
<div>
<!-- v-show vs v-if -->
<h1 v-show="showTitle">使用v-show的标题</h1>
<h1 v-if="showTitle">使用v-if的标题</h1>
<!-- 切换按钮 -->
<button @click="showTitle = !showTitle">
{{ showTitle ? '隐藏' : '显示' }}标题
</button>
<!-- 性能对比示例 -->
<div class="performance-demo">
<div v-show="toggle" class="expensive-component">
v-show: 频繁切换时性能更好
</div>
<div v-if="toggle" class="expensive-component">
v-if: 初始渲染成本更低
</div>
</div>
<button @click="toggle = !toggle">快速切换</button>
</div>
</template>
<script>
export default {
data() {
return {
showTitle: true,
toggle: true
}
}
}
</script>
<style scoped>
.expensive-component {
padding: 20px;
margin: 10px;
border: 1px solid #ccc;
background: #f9f9f9;
}
</style>
v-for列表渲染
<template>
<div>
<!-- 遍历数组 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.message }}
</li>
</ul>
<!-- 带索引的遍历 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.message }}
</li>
</ul>
<!-- 遍历对象 -->
<ul>
<li v-for="value in object" :key="value">
{{ value }}
</li>
</ul>
<!-- 遍历对象(带键名) -->
<ul>
<li v-for="(value, name) in object" :key="name">
{{ name }}: {{ value }}
</li>
</ul>
<!-- 遍历对象(带索引) -->
<ul>
<li v-for="(value, name, index) in object" :key="name">
{{ index }}. {{ name }}: {{ value }}
</li>
</ul>
<!-- 遍历数字 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
<!-- template包装 -->
<template v-for="item in items" :key="item.id">
<li>{{ item.message }}</li>
<li class="divider" role="presentation"></li>
</template>
<!-- 嵌套循环 -->
<ul v-for="set in sets" :key="set.id">
<li v-for="n in even(set)" :key="n">{{ n }}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, message: 'Foo' },
{ id: 2, message: 'Bar' },
{ id: 3, message: 'Baz' }
],
object: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
},
sets: [
{ id: 1, numbers: [1, 2, 3, 4, 5] },
{ id: 2, numbers: [6, 7, 8, 9, 10] }
]
}
},
methods: {
even(set) {
return set.numbers.filter(number => number % 2 === 0)
}
}
}
</script>
v-model双向绑定
<template>
<div>
<!-- 文本输入 -->
<input v-model="message" placeholder="编辑我">
<p>消息是: {{ message }}</p>
<!-- 多行文本 -->
<textarea v-model="multilineMessage" placeholder="添加多行文本"></textarea>
<p style="white-space: pre-line;">{{ multilineMessage }}</p>
<!-- 复选框 -->
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<!-- 多个复选框 -->
<div>
<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>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<br>
<span>选中的名字: {{ checkedNames }}</span>
</div>
<!-- 单选按钮 -->
<div>
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>选中: {{ picked }}</span>
</div>
<!-- 选择框 -->
<div>
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>选中: {{ selected }}</span>
</div>
<!-- 多选 -->
<div>
<select v-model="multiSelected" multiple style="width: 50px;">
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<br>
<span>选中: {{ multiSelected }}</span>
</div>
<!-- 动态选项 -->
<select v-model="dynamicSelected">
<option v-for="option in options" :value="option.value" :key="option.value">
{{ option.text }}
</option>
</select>
<span>选中: {{ dynamicSelected }}</span>
</div>
</template>
<script>
export default {
data() {
return {
message: '',
multilineMessage: '',
checked: false,
checkedNames: [],
picked: '',
selected: '',
multiSelected: [],
dynamicSelected: 'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
}
}
}
</script>
v-model修饰符
<template>
<div>
<!-- .lazy修饰符 -->
<input v-model.lazy="lazyMsg" placeholder="失去焦点时更新">
<p>{{ lazyMsg }}</p>
<!-- .number修饰符 -->
<input v-model.number="age" type="number" placeholder="自动转换为数字">
<p>年龄: {{ age }} (类型: {{ typeof age }})</p>
<!-- .trim修饰符 -->
<input v-model.trim="trimmedMsg" placeholder="自动去除首尾空格">
<p>"{{ trimmedMsg }}"</p>
<!-- 组合修饰符 -->
<input v-model.lazy.trim="combinedMsg" placeholder="组合修饰符">
<p>"{{ combinedMsg }}"</p>
</div>
</template>
<script>
export default {
data() {
return {
lazyMsg: '',
age: 0,
trimmedMsg: '',
combinedMsg: ''
}
}
}
</script>
3.3 事件处理
基本事件监听
<template>
<div>
<!-- 基本事件处理 -->
<button @click="counter += 1">增加 1</button>
<p>按钮被点击了 {{ counter }} 次。</p>
<!-- 方法事件处理器 -->
<button @click="greet">问候</button>
<!-- 内联处理器中的方法 -->
<button @click="say('hi')">说 hi</button>
<button @click="say('what')">说 what</button>
<!-- 访问原始DOM事件 -->
<button @click="warn('表单还不能提交。', $event)">
提交
</button>
<!-- 多个事件处理器 -->
<button @click="one($event), two($event)">
多个处理器
</button>
</div>
</template>
<script>
export default {
data() {
return {
counter: 0,
name: 'Vue.js'
}
},
methods: {
greet(event) {
alert('Hello ' + this.name + '!')
if (event) {
alert(event.target.tagName)
}
},
say(message) {
alert(message)
},
warn(message, event) {
if (event) {
event.preventDefault()
}
alert(message)
},
one(event) {
console.log('第一个处理器', event)
},
two(event) {
console.log('第二个处理器', event)
}
}
}
</script>
事件修饰符
<template>
<div>
<!-- 阻止默认行为 -->
<form @submit.prevent="onSubmit">
<input type="submit" value="提交">
</form>
<!-- 阻止事件冒泡 -->
<div @click="divClick">
<button @click.stop="buttonClick">阻止冒泡</button>
</div>
<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat" href="#">串联修饰符</a>
<!-- 只有修饰符 -->
<form @submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<div @click.capture="doThis">捕获模式</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<div @click.self="doThat">只在自身触发</div>
<!-- 点击事件将只会触发一次 -->
<a @click.once="doThis">只触发一次</a>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<div @scroll.passive="onScroll">被动监听</div>
</div>
</template>
<script>
export default {
methods: {
onSubmit() {
console.log('表单提交')
},
divClick() {
console.log('div被点击')
},
buttonClick() {
console.log('按钮被点击')
},
doThat() {
console.log('执行操作')
},
doThis() {
console.log('执行这个')
},
onScroll() {
console.log('滚动事件')
}
}
}
</script>
按键修饰符
<template>
<div>
<!-- 按键修饰符 -->
<input @keyup.enter="submit" placeholder="按回车提交">
<input @keyup.tab="handleTab" placeholder="按Tab键">
<input @keyup.delete="handleDelete" placeholder="按删除键">
<input @keyup.esc="handleEscape" placeholder="按ESC键">
<input @keyup.space="handleSpace" placeholder="按空格键">
<input @keyup.up="handleUp" placeholder="按上箭头">
<input @keyup.down="handleDown" placeholder="按下箭头">
<input @keyup.left="handleLeft" placeholder="按左箭头">
<input @keyup.right="handleRight" placeholder="按右箭头">
<!-- 系统修饰键 -->
<input @keyup.ctrl="handleCtrl" placeholder="按Ctrl键">
<input @keyup.alt="handleAlt" placeholder="按Alt键">
<input @keyup.shift="handleShift" placeholder="按Shift键">
<input @keyup.meta="handleMeta" placeholder="按Meta键">
<!-- 组合按键 -->
<input @keyup.ctrl.enter="handleCtrlEnter" placeholder="Ctrl+Enter">
<input @keyup.alt.67="handleAltC" placeholder="Alt+C">
<!-- 精确修饰符 -->
<button @click.ctrl.exact="onCtrlClick">Ctrl+点击</button>
<button @click.exact="onClick">仅点击</button>
<!-- 鼠标按钮修饰符 -->
<button @click.left="onLeftClick">左键点击</button>
<button @click.right="onRightClick">右键点击</button>
<button @click.middle="onMiddleClick">中键点击</button>
</div>
</template>
<script>
export default {
methods: {
submit() {
console.log('提交表单')
},
handleTab() {
console.log('Tab键被按下')
},
handleDelete() {
console.log('删除键被按下')
},
handleEscape() {
console.log('ESC键被按下')
},
handleSpace() {
console.log('空格键被按下')
},
handleUp() {
console.log('上箭头被按下')
},
handleDown() {
console.log('下箭头被按下')
},
handleLeft() {
console.log('左箭头被按下')
},
handleRight() {
console.log('右箭头被按下')
},
handleCtrl() {
console.log('Ctrl键被按下')
},
handleAlt() {
console.log('Alt键被按下')
},
handleShift() {
console.log('Shift键被按下')
},
handleMeta() {
console.log('Meta键被按下')
},
handleCtrlEnter() {
console.log('Ctrl+Enter被按下')
},
handleAltC() {
console.log('Alt+C被按下')
},
onCtrlClick() {
console.log('Ctrl+点击')
},
onClick() {
console.log('仅点击')
},
onLeftClick() {
console.log('左键点击')
},
onRightClick() {
console.log('右键点击')
},
onMiddleClick() {
console.log('中键点击')
}
}
}
</script>
3.4 计算属性和侦听器
计算属性
<template>
<div>
<!-- 基本计算属性 -->
<p>原始消息: "{{ message }}"</p>
<p>计算后的反转消息: "{{ reversedMessage }}"</p>
<!-- 计算属性 vs 方法 -->
<p>计算属性: {{ reversedMessage }}</p>
<p>方法: {{ reverseMessage() }}</p>
<!-- 复杂计算属性 -->
<div>
<h3>购物车</h3>
<div v-for="item in cartItems" :key="item.id">
{{ item.name }} - 数量: {{ item.quantity }} - 价格: ${{ item.price }}
</div>
<p><strong>总价: ${{ totalPrice }}</strong></p>
<p><strong>商品总数: {{ totalQuantity }}</strong></p>
</div>
<!-- 计算属性的getter和setter -->
<div>
<p>全名: {{ fullName }}</p>
<input v-model="firstName" placeholder="名">
<input v-model="lastName" placeholder="姓">
<input v-model="fullName" placeholder="全名">
</div>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue.js',
firstName: 'John',
lastName: 'Doe',
cartItems: [
{ id: 1, name: '苹果', quantity: 3, price: 2.5 },
{ id: 2, name: '香蕉', quantity: 2, price: 1.8 },
{ id: 3, name: '橙子', quantity: 1, price: 3.2 }
]
}
},
computed: {
// 基本计算属性
reversedMessage() {
return this.message.split('').reverse().join('')
},
// 复杂计算属性
totalPrice() {
return this.cartItems.reduce((total, item) => {
return total + (item.quantity * item.price)
}, 0).toFixed(2)
},
totalQuantity() {
return this.cartItems.reduce((total, item) => {
return total + item.quantity
}, 0)
},
// 带getter和setter的计算属性
fullName: {
get() {
return this.firstName + ' ' + this.lastName
},
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
},
methods: {
reverseMessage() {
return this.message.split('').reverse().join('')
}
}
}
</script>
侦听器
<template>
<div>
<!-- 基本侦听器 -->
<div>
<p>问一个 yes/no 问题:</p>
<input v-model="question">
<p>{{ answer }}</p>
</div>
<!-- 深度侦听 -->
<div>
<h3>用户信息</h3>
<input v-model="user.name" placeholder="姓名">
<input v-model="user.age" type="number" placeholder="年龄">
<input v-model="user.email" placeholder="邮箱">
<p>用户信息变化次数: {{ userChangeCount }}</p>
</div>
<!-- 数组侦听 -->
<div>
<h3>待办事项</h3>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加待办事项">
<button @click="addTodo">添加</button>
<ul>
<li v-for="(todo, index) in todos" :key="index">
{{ todo }}
<button @click="removeTodo(index)">删除</button>
</li>
</ul>
<p>待办事项变化次数: {{ todoChangeCount }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
question: '',
answer: '问题通常以问号结尾。;-)',
user: {
name: '',
age: 0,
email: ''
},
userChangeCount: 0,
todos: ['学习Vue.js', '构建项目'],
todoChangeCount: 0,
newTodo: ''
}
},
watch: {
// 基本侦听器
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
},
// 深度侦听对象
user: {
handler(newUser, oldUser) {
console.log('用户信息发生变化:', newUser)
this.userChangeCount++
},
deep: true
},
// 侦听对象的特定属性
'user.name'(newName, oldName) {
console.log(`用户名从 ${oldName} 变为 ${newName}`)
},
// 侦听数组
todos: {
handler(newTodos, oldTodos) {
console.log('待办事项发生变化:', newTodos)
this.todoChangeCount++
},
deep: true
},
// 立即执行的侦听器
question: {
handler(newQuestion) {
console.log('问题:', newQuestion)
},
immediate: true
}
},
methods: {
getAnswer() {
this.answer = '思考中...'
setTimeout(() => {
this.answer = Math.random() > 0.5 ? '是的' : '不是'
}, 1000)
},
addTodo() {
if (this.newTodo.trim()) {
this.todos.push(this.newTodo.trim())
this.newTodo = ''
}
},
removeTodo(index) {
this.todos.splice(index, 1)
}
}
}
</script>
3.5 Class与Style绑定
Class绑定
<template>
<div>
<!-- 对象语法 -->
<div :class="{ active: isActive, 'text-danger': hasError }">
对象语法绑定Class
</div>
<!-- 绑定数据对象 -->
<div :class="classObject">绑定数据对象</div>
<!-- 绑定计算属性 -->
<div :class="computedClassObject">绑定计算属性</div>
<!-- 数组语法 -->
<div :class="[activeClass, errorClass]">数组语法</div>
<!-- 数组中使用对象语法 -->
<div :class="[{ active: isActive }, errorClass]">数组中的对象语法</div>
<!-- 数组中使用三元表达式 -->
<div :class="[isActive ? activeClass : '', errorClass]">三元表达式</div>
<!-- 组件上的class -->
<my-component :class="{ active: isActive }"></my-component>
<!-- 控制按钮 -->
<div class="controls">
<button @click="isActive = !isActive">切换Active</button>
<button @click="hasError = !hasError">切换Error</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: false,
activeClass: 'active',
errorClass: 'text-danger'
}
},
computed: {
classObject() {
return {
active: this.isActive && !this.hasError,
'text-danger': this.hasError && this.hasError.type === 'fatal'
}
},
computedClassObject() {
return {
active: this.isActive,
'text-danger': this.hasError,
'font-bold': this.isActive && this.hasError
}
}
}
}
</script>
<style scoped>
.active {
background-color: #42b983;
color: white;
padding: 10px;
margin: 5px;
}
.text-danger {
color: #e74c3c;
border: 2px solid #e74c3c;
}
.font-bold {
font-weight: bold;
}
.controls {
margin-top: 20px;
}
.controls button {
margin-right: 10px;
padding: 5px 10px;
}
</style>
Style绑定
<template>
<div>
<!-- 对象语法 -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">
对象语法绑定Style
</div>
<!-- 绑定样式对象 -->
<div :style="styleObject">绑定样式对象</div>
<!-- 数组语法 -->
<div :style="[baseStyles, overridingStyles]">数组语法</div>
<!-- 自动添加前缀 -->
<div :style="{ transform: 'rotate(' + rotation + 'deg)' }">
自动添加浏览器前缀
</div>
<!-- 多重值 -->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }">
多重值(浏览器会选择支持的最后一个值)
</div>
<!-- 动态样式示例 -->
<div class="dynamic-box" :style="dynamicStyles">
动态样式盒子
</div>
<!-- 控制面板 -->
<div class="controls">
<label>
颜色:
<input type="color" v-model="activeColor">
</label>
<label>
字体大小:
<input type="range" min="12" max="48" v-model="fontSize">
{{ fontSize }}px
</label>
<label>
旋转角度:
<input type="range" min="0" max="360" v-model="rotation">
{{ rotation }}°
</label>
<label>
背景色:
<input type="color" v-model="backgroundColor">
</label>
<label>
边框宽度:
<input type="range" min="0" max="10" v-model="borderWidth">
{{ borderWidth }}px
</label>
</div>
</div>
</template>
<script>
export default {
data() {
return {
activeColor: '#42b983',
fontSize: 16,
rotation: 0,
backgroundColor: '#f0f0f0',
borderWidth: 2
}
},
computed: {
styleObject() {
return {
color: this.activeColor,
fontSize: this.fontSize + 'px',
fontWeight: 'bold',
padding: '10px',
margin: '5px 0'
}
},
baseStyles() {
return {
padding: '10px',
margin: '5px 0',
border: '1px solid #ccc'
}
},
overridingStyles() {
return {
color: this.activeColor,
fontSize: this.fontSize + 'px'
}
},
dynamicStyles() {
return {
backgroundColor: this.backgroundColor,
color: this.activeColor,
fontSize: this.fontSize + 'px',
border: `${this.borderWidth}px solid ${this.activeColor}`,
transform: `rotate(${this.rotation}deg)`,
transition: 'all 0.3s ease',
padding: '20px',
margin: '10px 0',
textAlign: 'center',
borderRadius: '8px'
}
}
}
}
</script>
<style scoped>
.controls {
margin-top: 20px;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.controls label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
.controls input {
margin-left: 10px;
}
.dynamic-box {
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
本章小结
本章我们学习了Vue.js的核心语法:
- 模板语法:文本插值、HTML插值、属性绑定
- 指令系统:v-if、v-show、v-for、v-model等核心指令
- 事件处理:事件监听、事件修饰符、按键修饰符
- 计算属性:基本用法、getter/setter、与方法的区别
- 侦听器:基本侦听、深度侦听、立即执行
- 样式绑定:Class绑定、Style绑定的多种语法
下一章预告
下一章我们将学习Vue.js的组件系统,包括: - 组件的定义和使用 - 组件间通信 - 插槽系统 - 动态组件
练习题
基础练习
模板语法练习:
- 创建一个用户信息展示组件
- 使用插值语法显示用户数据
- 实现条件渲染显示不同状态
列表渲染练习:
- 创建一个待办事项列表
- 实现添加、删除、标记完成功能
- 使用v-for渲染列表项
进阶练习
表单处理练习:
- 创建一个用户注册表单
- 使用v-model绑定各种表单元素
- 实现表单验证和提交
计算属性练习:
- 创建一个购物车组件
- 使用计算属性计算总价和商品数量
- 实现商品筛选和排序功能
提示:多练习这些基础语法,它们是Vue.js开发的基石。