主题
指令(下)、计算属性与侦听器(一)
本页对应 指令(下)、计算属性与侦听器,共 8 段视频,约 1 小时 24 分。
学习目标
- 能说明
reduce的两种语法及回调参数含义,并使用带初始值的写法完成普通数组和对象数组的求和。 - 能正确使用按键修饰符
.enter、.esc、.49,并验证仅在指定按键触发时执行事件处理函数。 - 能使用事件修饰符
.stop、.prevent及其连用形式,分别阻止冒泡和默认行为,并说明适用场景。 - 能使用
v-model的.trim、.number、.lazy修饰符处理表单输入,并验证收集到的数据格式与更新时机。 - 能使用
v-bind:class的对象语法和数组语法动态添加、移除类名,并通过布尔值或表达式控制类名切换。 - 能完成 tab 栏切换案例:使用
v-for渲染导航、用currentIndex控制active类,并实现点击切换高亮。
知识点
学习目标与回顾:从 reduce 到 Vue 指令
本节先补指令细节,再进入计算属性、侦听器和综合案例。转写稿先用 reduce 做回顾:它常用于数组求和。为什么需要:购物车总价、总数量都要累加;怎么想:reduce 自带循环,把“上一轮累加结果”和“当前元素”交给回调;怎么做:
js
const arr = [3, 5, 2, 7]
const sum = arr.reduce((total, cur) => total + cur, 0)带初始值 0 更通用。无初始值时,total 初始是数组第一个元素,cur 从第二个开始;带初始值会多一轮,但对象数组求和必须给初始值,否则第一轮 total 是对象,total + cur.number 会出错。易错点:不要以为无初始值永远够用;对象数组求和时初始值写 0。
指令修饰符:按键修饰符
为什么需要:键盘事件里常要判断“是否回车”,以前要在方法里写 e.key === 'Enter';怎么想:修饰符就是指令后的“小后缀”,让判断更短;怎么做:
html
<input @keyup.enter="fn">
<input @keyup.esc="fn">
<input @keyup.49="fn">不加修饰符时,按任何键都触发;加 .enter 后只有回车触发,.esc 只有 Esc 触发,.49 表示 keyCode 为 49 的键(大键盘数字 1)。易错点:按键修饰符只能用于键盘事件,不能乱用到 input 等非键盘事件;.49 这类编号要查 keyCode 对照表,不能凭感觉写。
事件修饰符:.stop 与 .prevent
为什么需要:父子盒子都绑点击时,点子盒子会冒泡触发父盒子;a 标签、表单提交有默认行为,开发中常要拦住。怎么想:原生用 stopPropagation、preventDefault,Vue 用修饰符更直观;怎么做:
html
<div @click="fn1">
<div @click.stop="fn2"></div>
</div>
<a href="https://www.baidu.com" @click.prevent="fn3">跳转</a>阻止冒泡加 .stop,阻止默认行为加 .prevent,两者可连写。易错点:绑定的事件方法必须存在,否则点击会报“找不到方法”;修饰符不能交叉乱用,每个指令有自己的修饰符体系。
v-model 修饰符:.trim、.number、.lazy
为什么需要:用户输入可能带首尾空格;数字输入框收集到的是字符串;实时更新有时太频繁。怎么想:把常见处理交给修饰符;怎么做:
html
<input v-model.trim="username">
<input v-model.number="age">
<input v-model.lazy="phone">.trim 去首尾空格;.number 把输入的数字字符串转成数字类型,但只对数字输入有效;.lazy 在失去焦点/change 时才更新数据。易错点:.number 不是“限制只能输入数字”,限制输入要用正则;不要写 v-model.stop 这类不存在的组合。
v-bind 控制 class:对象与数组
为什么需要:tab 栏切换、菜单高亮要动态加/删类名,不能把类名写死。怎么想::class 的值可以是对象或数组;怎么做:
html
<div :class="{ box: true, pink: flag }"></div>
<div :class="['box', 'pink', flag ? 'bg' : '']"></div>对象格式中,类名作键,布尔值决定加不加;数组格式中每个元素是类名,也可用三元表达式。易错点:加了冒号后,值是变量或表达式;对象键一般加引号更稳;布尔表达式结果也是布尔值,不一定要写死 true/false。
tab 栏切换案例:current 控制高亮
为什么需要:三个标签只有一个高亮,点击谁谁高亮。怎么想:循环渲染时用 index,再用 current 记录当前高亮项;怎么做:
html
<li v-for="(item, index) in list" :key="item.id"
:class="{ active: index === current }"
@click="current = index">
{{ item.name }}
</li>调试时先在控制台改 current,看高亮是否切换;验证后再绑定点击。易错点:不要写死 index === 0,否则永远只有第一个高亮;current 默认设为 0,它必须是变量才有机会改。
计算属性、侦听器与综合案例目标
本节笔记还列出计算属性:基础语法、计算属性 vs 方法、完整写法、成绩案例;侦听器:基础写法、完整写法。转写稿只做了目标介绍:计算属性用于“通过计算得到的值”,如商品总价,原数据一变会自动重算;侦听器用于监听数据变化后做事,如在线翻译输入变化后请求结果。易错点:完整写法容易因先学基础语法而产生“先入为主”的畏难感,后续要按步骤对照。综合案例将演示渲染、删除、修改数量、全选/反选、统计总价和持久化,把指令、计算属性、侦听器串起来。
什么是指令修饰符
为什么需要:Vue 指令本身已经能完成事件绑定、双向绑定等操作,但很多细节处理(如只响应回车、阻止冒泡、去掉输入空格)如果每次都在方法里写原生代码,会重复且啰嗦。
怎么想:指令修饰符就是通过“.”给指令加后缀,不同后缀封装不同处理操作,从而简化代码。
怎么做:把修饰符写在指令后面,例如 @keyup.enter、v-model.trim、@click.stop。
易错点:不同指令有各自支持的修饰符,不能交叉乱用,例如 v-model.stop 并不存在;修饰符要写在正确位置。
按键修饰符:@keyup.enter
为什么需要:搜索框、登录框常要求“按回车才触发”,不加修饰符时按任意键都会触发。
怎么想:按键修饰符只能用于键盘事件,如 keyup、keydown、keypress,不能用于 input 这类输入事件。
怎么做:写 @keyup.enter="fn",表示只有回车键抬起时才执行 fn。转写稿中还演示了 .esc 和 .49:.49 表示 keyCode 为 49 的键,即大键盘数字 1。
易错点:不加修饰符时按 A、B、中文都会触发;加了 .enter 后字母和中文不触发,只有回车触发。keyCode 不直观,优先用 .enter、.esc 这类别名。
js
<div id="app">
<h3>@keyup.enter → 监听键盘回车事件</h3>
<input v-model="username" type="text">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: ''
},
methods: {
}
})
</script>v-model 修饰符:.trim 与 .number
为什么需要:用户输入账号时可能前后带空格,直接提交会导致“账号密码错误”;输入年龄、价格时,v-model 默认收集到的是字符串,做求和计算可能出问题。
怎么想:在双向绑定处加后缀,让 Vue 在收集值时自动处理。
怎么做:v-model.trim 去除首尾空格;v-model.number 把输入值转成数字。转写稿调试时,输入“老汤 ”后,收集到的 username 两边没有空格;年龄输入 30 时默认是字符串,加 .number 后变成数字 30。
易错点:.trim 只去首尾空格,不去中间空格;.number 只在输入数字时有效,输入字母不能强制转成数字;不要写成 v-model.stop 这种不存在的组合。
事件修饰符:.stop 与 .prevent
为什么需要:父子元素都绑点击事件时,点子元素会冒泡触发父元素;a 标签点击会跳转、表单提交会刷新页面,原生要用 e.stopPropagation()、e.preventDefault()。
怎么想:Vue 把这些常用操作封装成事件修饰符,直接加在事件名后。
怎么做:@click.stop 阻止冒泡;@click.prevent 阻止默认行为;@click.stop.prevent 可以连用。转写稿调试时,点子盒子会同时弹出父、子提示,给子盒子加 .stop 后只弹出子提示;a 标签不加 .prevent 会跳转百度,加了才阻止跳转。
易错点:.stop 不阻止默认行为,.prevent 不阻止冒泡;连用时写成 .stop.prevent;事件处理函数必须存在,否则会报“找不到方法”。下面笔记代码中 a 标签演示阻止默认行为时,实际应写 @click.prevent。
js
<style>
.father {
width: 200px;
height: 200px;
background-color: pink;
margin-top: 20px;
}
.son {
width: 100px;
height: 100px;
background-color: skyblue;
}
</style>
<div id="app">
<h3>v-model修饰符 .trim .number</h3>
姓名:<input v-model="username" type="text"><br>
年纪:<input v-model="age" type="text"><br>
<h3>@事件名.stop → 阻止冒泡</h3>
<div @click="fatherFn" class="father">
<div @click="sonFn" class="son">儿子</div>
</div>
<h3>@事件名.prevent → 阻止默认行为</h3>
<a @click href="http://www.baidu.com">阻止默认行为</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
age: '',
},
methods: {
fatherFn () {
alert('老父亲被点击了')
},
sonFn (e) {
// e.stopPropagation()
alert('儿子被点击了')
}
}
})
</script>为什么需要 v-bind 操作 class
为什么需要:原生开发里改类名常用 classList.add/remove,例如 tab 栏切换时要给当前项加 active。Vue 中数据一变页面就更新,所以更希望“类名加不加”由数据决定,而不是手动操作 DOM。class 和 style 本质上也是属性,因此可以用 v-bind 控制,简写为 :。
怎么想:把“某个类是否出现”映射成布尔值,或把“要添加的类名列表”映射成数组。v-bind:class 可以写成 :class,后面必须是变量、表达式、对象或数组。
怎么做:
html
<div :class="对象/数组">这是一个div</div>易错点:写成 :class="pink" 时,pink 会被当成变量查找,不是类名字符串;属性值里不要加双大括号,如 :class="{{ flag }}" 是错的。
对象语法:键是类名,值是布尔值
为什么需要:一个类名需要来回切换时,用对象最直观。例如 pink 背景色是否出现,只取决于一个布尔值。
怎么想:对象语法中“键就是类名,值就是布尔值”。值为 true 就添加这个类,为 false 就不添加。
怎么做:
html
<div class="box" :class="{ 类名1: 布尔值, 类名2: 布尔值 }"></div>转写稿中的调试过程是:先给盒子写 box,再用 :class="{ box: true, pink: true, big: true }",刷新后三个类都生效;然后把 pink 的值换成数据项 flag,在 data 中写 flag: true。此时在控制台把 flag 改成 false,pink 类就消失,说明类名可以被动态切换。
易错点:对象的值不要写成字符串 "true";类名作为键时,若含短横线要加引号;布尔值也可以写判断表达式,例如 { active: index === 0 },表达式结果仍是布尔值。
数组语法:把类名放进数组批量控制
为什么需要:批量添加或删除多个类时,数组比对象更简洁。数组里每个元素都是一个类名,Vue 会把它们作为 class 列表添加。
怎么想:数组本质就是 class 列表。想加 box、pink、big,就把三个类名字符串放进数组。
怎么做:
html
<div class="box" :class="[ 类名1, 类名2, 类名3 ]"></div>转写稿中写成 :class="['box', 'pink', 'big']" 后,第二个盒子会同时拥有三个类。若只想控制 pink,可用三元表达式:flag ? 'pink' : '',flag 为 true 时加 pink,否则给空字符串。
易错点:数组元素应是类名字符串,不要写成对象;空字符串不会添加类;数组里用变量时,要确保变量值是类名字符串。
代码练习与动态切换推导
原笔记练习代码保留如下:
html
<style>
.box {
width: 200px;
height: 200px;
border: 3px solid #000;
font-size: 30px;
margin-top: 10px;
}
.pink {
background-color: pink;
}
.big {
width: 300px;
height: 300px;
}
</style>
<div id="app">
<!--绑定对象-->
<div class="box">黑马程序员</div>
<!--绑定数组-->
<div class="box">黑马程序员</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>为什么需要:练习要把“静态类名”改成“动态类名”,否则无法体会响应式切换。
怎么想:第一个盒子用对象语法,第二个盒子用数组语法;把是否显示 pink 交给 flag。
怎么做:补全为:
html
<div id="app">
<div class="box" :class="{ box: true, pink: flag, big: true }">黑马程序员</div>
<div class="box" :class="['box', flag ? 'pink' : '', 'big']">黑马程序员</div>
</div>
<script>
const app = new Vue({
el: '#app',
data: { flag: true }
})
</script>调试时改 flag 为 false,两个盒子的粉色背景都会消失,说明对象和数组都能动态控制类名。
tab 栏切换的推导同理:先用 v-for 渲染列表,再写 :class="{ active: index === 0 }",此时只有第一项高亮;但 0 写死了,点第二项无法切换。于是把条件改成 index === currentIndex,data 中 currentIndex: 0,点击项写 @click="currentIndex = index"。这样点击谁,currentIndex 就变成谁的索引,active 类随之切换。
易错点:不要把索引写死成 0,否则只能默认第一项高亮;v-for 要加唯一 key;类名 active 拼写要一致;:class 后面是表达式,不是普通字符串。
需求:点击哪个 tab,哪个 tab 高亮
为什么需要:京东秒杀页的 tab 栏不能只靠静态样式,用户点击“每日特价”或“品类秒杀”时,高亮要跟着移动,否则用户不知道当前选中了哪一项。
怎么想:先确认高亮不是 li 天生自带的,而是由 active 类名控制的;再用数据驱动的方式,让“当前高亮下标”变化时,类名自动切换。
怎么做:保留下面准备代码,先观察第一个 a 上的 class="active",它让第一个 tab 变红底白字。
易错点:不要把高亮理解成“第一个 li 本来就这样”,转写稿里通过审查元素把 active 双击删掉后,第一个 tab 立刻变成普通样式,说明高亮就是类名在起作用。
准备代码(保留):
html
<style>
* {
margin: 0;
padding: 0;
}
ul {
display: flex;
border-bottom: 2px solid #e01222;
padding: 0 10px;
}
li {
width: 100px;
height: 50px;
line-height: 50px;
list-style: none;
text-align: center;
}
li a {
display: block;
text-decoration: none;
font-weight: bold;
color: #333333;
}
li a.active {
background-color: #e01222;
color: #fff;
}
</style>
<div id="app">
<ul>
<li><a class="active" href="#">京东秒杀</a></li>
<li><a href="#">每日特价</a></li>
<li><a href="#">品类秒杀</a></li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
list: [
{ id: 1, name: '京东秒杀' },
{ id: 2, name: '每日特价' },
{ id: 3, name: '品类秒杀' }
]
}
})
</script>用 v-for 动态渲染 tab
为什么需要:data.list 已经有三项数据,如果手写三个 li,数据一改就要同步改模板,重复且容易漏。
怎么想:把 li 交给 v-for 循环生成,item.name 负责显示文字,item.id 作为唯一 key。
怎么做:把原来三个 li 换成一个循环 li:
html
<ul>
<li v-for="(item, index) in list" :key="item.id">
<a href="#">{{ item.name }}</a>
</li>
</ul>易错点:v-for 要写在 li 上,不要写到 a 上;index 是循环的第二个参数,后面切换高亮要用它;key 用唯一的 item.id,不要用重复值。
准备高亮下标:从写死到变量
为什么需要:如果直接写 :class="{ active: index === 0 }",确实能让第一个 tab 默认高亮,但这个 0 是写死的,点击第二个、第三个时没法改。转写稿里明确说:“你这样的话,你没法改……这个不要写死,用变量。”
怎么想:需要一个变量记录“当前高亮的是哪一个 tab”,默认第一个高亮,所以初始值设为 0。
怎么做:在 data 里加 currentIndex: 0,并把 a 的 class 改成对象绑定:
html
<li v-for="(item, index) in list" :key="item.id">
<a :class="{ active: currentIndex === index }" href="#">{{ item.name }}</a>
</li>js
data: {
list: [
{ id: 1, name: '京东秒杀' },
{ id: 2, name: '每日特价' },
{ id: 3, name: '品类秒杀' }
],
currentIndex: 0
}易错点:active 是类名,值是布尔表达式;不要写成 :class="active",那会去 data 里找 active 变量。也不要同时保留原来的 class="active",否则第一个 tab 会永远高亮。
点击切换:@click 修改 currentIndex
为什么需要:高亮要跟着用户点击移动,不能只靠默认第一个。
怎么想:点击时把当前循环的 index 赋给 currentIndex,currentIndex 一变,:class 的布尔结果就变,高亮自动切换。
怎么做:给 li 加点击事件:
html
<li
v-for="(item, index) in list"
:key="item.id"
@click="currentIndex = index"
>
<a :class="{ active: currentIndex === index }" href="#">{{ item.name }}</a>
</li>调试时可以按转写稿的思路:先刷新,默认第一个高亮;临时在控制台把 currentIndex 改成 1、2,观察高亮是否移动到第二、第三个;确认 class 绑定没问题后,再绑定点击事件。
易错点:点击事件里不要写成 currentIndex = item,应该用 index;如果写在 methods 里,要记得 this.currentIndex = index;点击 a 时 href="#" 可能让地址栏变化,若不想变化可加 .prevent,但本例核心是切换高亮。
小结:整条链路
数据 list 负责渲染 tab,currentIndex 负责记录当前高亮下标,:class="{ active: currentIndex === index }" 负责根据下标切换类名,@click="currentIndex = index" 负责点击时更新下标。这样就从“静态写死 active”变成了“数据驱动高亮”,也避免了写死 index === 0 后无法切换的问题。
先理解:style 也是属性,所以用 v-bind
class 和 style 本质上都是 HTML 属性,因此都可以交给 v-bind 处理。v-bind:style 的简写是 :style。它适合“只临时改某一个样式”的场景,例如进度条宽度可能是 25%、50%、75%、100%,如果为每个百分比都写一个类名,类名会很多;而用行内样式绑定,只需要改一个数据值。
怎么想:把 CSS 属性名作为对象的键,把 CSS 属性值作为对象的值;值可以写死,也可以来自 data 中的变量或表达式。怎么做:在标签上写 :style="{ width: progress + '%' }",再在 data 中维护 progress。易错点:不要把 :style 和双大括号混用;属性值要带单位;CSS 属性名建议用驼峰,如 backgroundColor,若写 background-color 则要加引号。
基本语法与代码练习
笔记给出的语法是:
html
<div class="box" :style="{ CSS属性名1: CSS属性值, CSS属性名2: CSS属性值 }"></div>下面的代码先保留笔记原样,用来观察静态盒子:
html
<style>
.box {
width: 200px;
height: 200px;
background-color: rgb(187, 150, 156);
}
</style>
<div id="app">
<div class="box"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>要让它“动”起来,可以把宽高、背景色放进 data,再在标签上绑定::style="{ width: boxWidth, height: boxHeight, backgroundColor: boxColor }"。这样改的是数据,页面样式会跟着更新。易错点:200px 是字符串,不能只写 200;对象里的键是 CSS 属性名,不是类名;不要写成 :style="{{ width: ... }}"。
进度条案例:把宽度交给数据
进度条最适合说明 :style 的价值:外层 .progress 固定宽度,内层 .inner 的宽度表示进度,文字也显示百分比。为什么需要:宽度是连续变化的,用行内样式绑定比写多个类名更直接。怎么想:data 里存一个 progress: 50,内层宽度写成 progress + '%',文字写成 {{ progress + '%' }};四个按钮点击时分别把 progress 改成 25、50、75、100。怎么做:保留下面结构和样式:
html
<style>
.progress {
height: 25px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
width: 50%;
height: 20px;
border-radius: 10px;
text-align: right;
position: relative;
background-color: #409eff;
background-size: 20px 20px;
box-sizing: border-box;
transition: all 1s;
}
.inner span {
position: absolute;
right: -20px;
bottom: -25px;
}
</style>
<div id="app">
<div class="progress">
<div class="inner">
<span>50%</span>
</div>
</div>
<button>设置25%</button>
<button>设置50%</button>
<button>设置75%</button>
<button>设置100%</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>补全时,把内层改成 :style="{ width: progress + '%' }",把 span 改成 {{ progress + '%' }},按钮分别加 @click="progress = 25" 等。注意:这里改的是 progress 数据,不是直接改 DOM;transition: all 1s 会让宽度变化有过渡;如果宽度写死成 50%,按钮就无法切换。
调试与易错点
调试时可以先在 data 中把 progress 设为 50,刷新看是否为一半;再在控制台把 progress 改成 25,观察内层宽度和文字是否同时变化。转写稿里强调“写死就没法改,要用变量”,进度条也是同理:宽度必须来自变量,按钮只负责改变量。易错点:百分比要拼成字符串;style 对象的值不要漏单位;按钮点击后方法或表达式要能改到 data;不要把 :style 写到类名上,也不要把 class 和 style 的绑定格式混用。
为什么要把 v-model 用到更多表单元素
笔记里说得很清楚:常见的表单元素都可以用 v-model 绑定关联,从而快速获取或设置表单元素的值;它会根据控件类型自动选取正确的方法来更新元素。转写稿里也强调,昨天只把 v-model 和 input 输入框配合了,今天要解决的是:下拉框 select、复选框 checkbox、单选框 radio、文本域 textarea 怎么用。为什么要这么做?因为表单不止一个输入框,如果每个控件都手写事件、手动取值,代码会散、容易漏;而 v-model 做的是双向绑定:变量一变,控件的 value/checked 变;控件一改,变量也变,方便获取表单值、清空输入框、做数据回填。
怎么想?先记住这张对应关系:
js
输入框 input:text ——> value
文本域 textarea ——> value
复选框 input:checkbox ——> checked
单选框 input:radio ——> checked
下拉菜单 select ——> value所以不要把所有控件都当成“绑 value”。复选框、单选框要看 checked,下拉菜单和文本框看 value。
先把准备代码放好
下面是本节笔记里的准备代码,先保留它,作为未绑定版本:
html
<style>
textarea {
display: block;
width: 240px;
height: 100px;
margin: 10px 0;
}
</style>
<div id="app">
<h3>小黑学习网</h3>
姓名:
<input type="text">
<br><br>
是否单身:
<input type="checkbox">
<br><br>
<!--
前置理解:
1. name: 给单选框加上 name 属性 可以分组 → 同一组互相会互斥
2. value: 给单选框加上 value 属性,用于提交给后台的数据
结合 Vue 使用 → v-model
-->
性别:
<input type="radio">男
<input type="radio">女
<br><br>
<!--
前置理解:
1. option 需要设置 value 值,提交给后台
2. select 的 value 值,关联了选中的 option 的 value 值
结合 Vue 使用 → v-model
-->
所在城市:
<select>
<option>北京</option>
<option>上海</option>
<option>成都</option>
<option>南京</option>
</select>
<br><br>
自我描述:
<textarea></textarea>
<button>立即注册</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>输入框与文本域:绑 value
为什么需要:姓名、自我描述都是字符串,输入后要能收集,也要能回填。
怎么想:input:text 和 textarea 都对应 value,直接用 v-model 绑 data 里的字符串。
怎么做:在准备代码上补成:
html
姓名:<input type="text" v-model="name">
自我描述:<textarea v-model="desc"></textarea>js
data: {
name: '',
desc: ''
}易错点:textarea 不要用双大括号写内容,应该用 v-model;data 初始值给空字符串,类型要和控件值一致。
复选框:绑 checked 的布尔
为什么需要:“是否单身”不是文本,而是“选中/未选中”的状态。
怎么想:checkbox 对应 checked,所以 v-model 绑一个布尔值。
怎么做:
html
是否单身:<input type="checkbox" v-model="isSingle">js
data: {
isSingle: false
}勾选后 isSingle 变为 true,取消变 false。易错点:不要把它当成绑 value 的输入框;初始值建议用 false。
单选框:name 分组 + value 提交
为什么需要:性别要互斥,而且提交给后台要能区分“男/女”。
怎么想:name 用来分组,同一组互相排斥;value 用来表示提交数据;v-model 绑选中的值。
怎么做:
html
性别:
<input type="radio" name="gender" value="男" v-model="gender">男
<input type="radio" name="gender" value="女" v-model="gender">女js
data: {
gender: ''
}易错点:忘写 name 可能不互斥;忘写 value 则提交数据不明确。笔记注释也特别提示了这两点。
下拉菜单:select 的 value 关联 option 的 value
为什么需要:城市选择要拿到选中项,并且能设置默认城市。
怎么想:select 的 value 关联了选中的 option 的 value,所以 option 要写 value。
怎么做:
html
所在城市:
<select v-model="city">
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="成都">成都</option>
<option value="南京">南京</option>
</select>js
data: {
city: ''
}易错点:option 不写 value 不符合本节笔记的前置理解;data 初始值最好和某个 option 的 value 对应,便于回填。
调试与易错点汇总
按转写稿的调试思路:页面输入后,在控制台看 app.name、app.desc 是否变化;控制台执行 app.name = '新值',看输入框是否更新;单选框切换后看 app.gender 是否等于对应 value;下拉菜单切换后看 app.city;复选框勾选后看 app.isSingle。这就是双向绑定的验证过程。
易错点再强调:v-model 主要用在表单元素上,用在 p、div 上没有实际意义;不同指令有各自的修饰符,不要乱搭配,例如不要写 v-model.stop 这种交叉用法;radio 要 name 和 value,select 的 option 要 value,checkbox 绑布尔状态,textarea 用 v-model。后续若用到 v-model.trim、v-model.number、v-model.lazy,再按修饰符规则单独学习即可。
课件图示








代码示例
示例一:指令修饰符(按键 / v-model / 事件)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>指令修饰符</title>
<style>
.father { width: 200px; height: 200px; background-color: pink; margin-top: 20px; }
.son { width: 100px; height: 100px; background-color: skyblue; }
</style>
</head>
<body>
<div id="app">
<h3>@keyup.enter → 监听键盘回车事件</h3>
<input v-model="username" @keyup.enter="sayHi" type="text">
<p>输入内容:{{ username }}</p>
<h3>v-model 修饰符 .trim .number .lazy</h3>
姓名:<input v-model.trim="name" type="text"><br>
年纪:<input v-model.number="age" type="text"><br>
手机:<input v-model.lazy="phone" type="text"><br>
<p>姓名:{{ name }},年纪:{{ age }},手机:{{ phone }}</p>
<h3>@事件名.stop → 阻止冒泡</h3>
<div @click="fatherFn" class="father">
<div @click.stop="sonFn" class="son">儿子</div>
</div>
<h3>@事件名.prevent → 阻止默认行为</h3>
<a @click.prevent href="http://www.baidu.com">阻止默认行为</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
name: '',
age: 20,
phone: ''
},
methods: {
sayHi () { alert('回车被按下了:' + this.username) },
fatherFn () { alert('老父亲被点击了') },
sonFn () { alert('儿子被点击了') }
}
})
</script>
</body>
</html>关键行与易错点
- 修饰符就是指令后面的“
.后缀”,@keyup.enter表示只有按回车才触发;.stop内部等价于e.stopPropagation(),.prevent等价于e.preventDefault(),两者可以连写成.stop.prevent。 @keyup.enter只能用于键盘事件(keyup/keydown),不能乱用到input事件上;每个指令有自己的修饰符,不能交叉使用,例如v-model.stop是不存在的。v-model.trim去掉首尾空格,v-model.number转数字——但只有输入的内容本身是数字时才有效,输入abc依旧是字符串;v-model.lazy是失去焦点(change)时才更新数据,不是实时更新。- 绑定了事件就必须提供对应方法,哪怕方法里什么都不写,否则点击时会报“找不到该方法”。
示例二:v-bind 操作 class(对象 / 数组)+ tab 切换高亮
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>v-bind 操作 class</title>
<style>
* { margin: 0; padding: 0; }
.box { width: 200px; height: 200px; border: 3px solid #000; font-size: 30px; margin-top: 10px; }
.pink { background-color: pink; }
.big { width: 300px; height: 300px; }
ul { display: flex; border-bottom: 2px solid #e01222; padding: 0 10px; }
li { width: 100px; height: 50px; line-height: 50px; list-style: none; text-align: center; }
li a { display: block; text-decoration: none; font-weight: bold; color: #333333; }
li a.active { background-color: #e01222; color: #fff; }
</style>
</head>
<body>
<div id="app">
<!-- 对象语法:键是类名,值是布尔值 -->
<div class="box" :class="{ pink: true, big: false }">对象语法</div>
<!-- 数组语法:数组中的每个类都会加到盒子上 -->
<div class="box" :class="['pink', 'big']">数组语法</div>
<ul>
<li v-for="(item, index) in list" :key="item.id">
<a :class="{ active: index === current }" @click="current = index" href="#">{{ item.name }}</a>
</li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
current: 0,
list: [
{ id: 1, name: '京东秒杀' },
{ id: 2, name: '每日特价' },
{ id: 3, name: '品类秒杀' }
]
}
})
</script>
</body>
</html>关键行与易错点
:class是v-bind:class的简写,值必须是变量或表达式;对象语法“键是类名、值是布尔值”,值为true才有这个类,数组语法则是把数组里所有类都加上。- 值可以是布尔表达式,如
index === current,结果本身就是布尔值,不必非得写true/false。 - 高亮必须用变量
current记录下标,不能写死index === 0,写死就永远切不动了。 v-for必须给:key,优先用唯一的id。- 属性值里不能加双大括号,写成
:class="{{ ... }}"是错误写法。
示例三:v-model 应用于其他表单元素
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>v-model 其他表单元素</title>
<style>
textarea { display: block; width: 240px; height: 100px; margin: 10px 0; }
</style>
</head>
<body>
<div id="app">
<h3>小黑学习网</h3>
姓名:<input v-model="username" type="text"><br><br>
是否单身:<input v-model="isSingle" type="checkbox"><br><br>
性别:
<input v-model="gender" type="radio" value="男">男
<input v-model="gender" type="radio" value="女">女<br><br>
所在城市:
<select v-model="city">
<option>北京</option>
<option>上海</option>
<option>成都</option>
<option>南京</option>
</select><br><br>
自我描述:<textarea v-model="desc"></textarea>
<button @click="submit">立即注册</button>
<p>{{ username }} / {{ isSingle }} / {{ gender }} / {{ city }} / {{ desc }}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
isSingle: false,
gender: '',
city: '',
desc: ''
},
methods: {
submit () {
console.log({
username: this.username,
isSingle: this.isSingle,
gender: this.gender,
city: this.city,
desc: this.desc
})
}
}
})
</script>
</body>
</html>关键行与易错点
v-model会按控件类型自动选取正确方式更新元素:输入框 / 文本域取value,复选框 / 单选框取checked,下拉菜单取value。- 单选框必须写
value,否则gender拿到的是null;同一组的单选框还要有相同的name才能互斥。 - 单个复选框绑定的是布尔值(
isSingle),如果是多个复选框则要绑定数组。 select绑定的值就是被选中option的value;option没写value时,取的是标签文本。data里要先声明对应字段并给初始值(如isSingle: false),否则初始渲染时绑定的属性不存在,容易出现视图不响应。
常见错误
1. 把按键修饰符用在非键盘事件上
- 现象:
@input.enter="fn"、@click.enter="fn"写了没反应,或者只要一输入就触发。 - 原因:
.enter/.esc/.49这类按键修饰符只能挂在keyup、keydown、keypress上,它判断的是事件对象的key/keyCode;input、click的事件对象里根本没有这个信息。 - 修法:把事件改成
@keyup.enter;如果确实要在输入框里响应回车,也还是用@keyup.enter,不要想着套在@input上。
2. 修饰符跨指令乱用
- 现象:写
v-model.stop、@click.trim、:class.number,要么报错,要么修饰符被无视、行为完全不对。 - 原因:修饰符是每个指令私有的,
stop/prevent属于事件指令,trim/number/lazy属于v-model,按键类属于键盘事件,彼此不通用。 - 修法:记不清就查 Vue2 官方文档对应指令的“修饰符”小节;写之前先问自己“这个修饰符是挂在哪个指令后面的”。
3. 指望 v-model.number 帮做输入校验
- 现象:输入
abc或12abc,拿到的还是字符串,total算出来是NaN或者字符串拼接。 - 原因:
.number只是“能用parseFloat转成数字时才转”,转不了就原样保留字符串,它不做拦截。 - 修法:
.number只当类型转换用;限制只能输数字要用正则或type="number"+ 提交前校验,两者职责分开。
4. :class 的对象/数组写法混用
- 现象:页面上类名没加上,或者 DOM 里出现
class="[object Object]"、class="true"。 - 原因:对象语法是
{ 类名: 布尔值 }(键是类名、值是开关),数组语法是[ '类名1', '类名2' ](元素是类名字符串),把布尔值或对象塞进数组就废了。 - 修法:要“一个类来回切”用对象;要“批量增删一批类”用数组;数组里想动态控制某个类,用三元
flag ? 'pink' : ''。
5. 高亮下标写死在模板里
- 现象:
:class="{ active: index === 0 }"能默认高亮第一个,但点谁都不切换。 - 原因:
0是字面量,没有任何东西能改它;模板里必须依赖一个可变的响应式变量。 - 修法:
data里声明currentIndex: 0,模板写index === currentIndex,点击时把currentIndex赋成当前index。
6. 计算属性里写副作用或不写 return
- 现象:页面上渲染出
undefined;或者在 computed 里改数据导致死循环 / 报 “Avoid mutating a prop” 之类。 - 原因:computed 的定位是“由依赖算出一个值”,必须有
return,且不能在里面赋值、发请求、改 DOM。 - 修法:computed 里只做纯计算并
return;要“数据一变就做事”(发请求、存 localStorage)交给watch。
7. 侦听对象内部属性却没开 deep
- 现象:
watch: { obj() {} }里改obj.name,回调不执行。 - 原因:默认只侦听引用变化,对象内部属性变了引用没变,触发不了。
- 修法:需要整体监听就写完整写法
{ handler() {}, deep: true };只关心某个字段就直接侦听那个字段(或字符串路径)。
练习
练习 1|修饰符速查表(10 分钟) 自己动手把下面 5 个需求各用一行模板 + 一个方法写出来,不许在方法体里写 e.stopPropagation() / e.preventDefault() / e.key === 'Enter': ① 输入框按回车才提交;② 点儿子盒子不触发爸爸的点击;③ 点 <a> 不跳转;④ 点按钮既不冒泡也不跳转;⑤ 姓名输入框自动去掉首尾空格、年龄输入框拿到的必须是数字。
验收:把方法体清空到只剩一句
console.log,功能依然成立。
练习 2|进度条(对象语法 + :style) 基于笔记里的 .progress/.inner 结构,用 data 里的 percent(默认 50)驱动内层宽度和文字;四个按钮分别设为 25/50/75/100。
加分:把
percent改成一个“能被按钮改、也能被输入框改”的变量,输入 33 时进度条也要跟着走。
练习 3|tab 高亮改造 把笔记里的京东秒杀 tab 改成:鼠标移入即高亮(@mouseenter),并且额外要求“高亮项文字后面追加一个 🔥”(提示:同一个 :class 对象里再加一个类名,或用数组 + 三元)。
验收:默认第一个高亮;移入谁谁高亮;刷新页面回到第一个。
练习 4|注册表单一次性收集 用 v-model 把姓名(text)、是否单身(checkbox,布尔)、性别(radio,值为“男/女”)、城市(select)、自我描述(textarea)全部绑到 data 上,点“立即注册”时 console.log 出一个完整对象。
关键点自查:radio 是否加了相同的
name和各自的value;select 的option是否都有value;checkbox 绑的是布尔还是数组,你自己想清楚要哪个。
练习 5|computed vs watch 分工 给练习 4 的表单加两件事: ① 用 computed 算出“已填写字段数 / 总字段数”(空字符串、null、false 都算未填); ② 用 watch 侦听“城市”的变化,一变就 console.log('城市改为:' + 新值),并把整份表单存进 localStorage,刷新后自动回填。
验收:改城市时只触发一次日志;改姓名时不触发城市日志;刷新后表单内容还在。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 回顾reduce方法 | 14:26 |
| 2 | 回顾和反馈 | 13:23 |
| 3 | 今日内容介绍(1-3) | 09:20 |
| 4 | 指令修饰符-按键修饰符(4-5) | 08:44 |
| 5 | 指令修饰符-事件修饰符(4-5) | 09:22 |
| 6 | 指令修饰符-v-model修饰符(4-5) | 10:41 |
| 7 | v-bind对class的控制(6-8) | 11:48 |
| 8 | 案例-tab栏切换案例(9) | 07:01 |
小结
- 修饰符是“指令的私有后缀”,位置永远紧跟在指令后面;跨指令使用一律无效,记不清就查官方文档对应指令那一节。
- 三类修饰符的归属要背下来:
@keyup.enter/.esc/.键码只给键盘事件;.stop/.prevent(可连写)只给事件指令;.trim/.number/.lazy只给v-model。 .trim解决“看不见的空格坑”,.number只做类型转换、不做校验,.lazy把“实时同步”改成“失焦同步”——三者都是收集时机/格式的微调,不是校验手段。:class和:style的本质还是v-bind:对象语法适合单个类名开关切换,数组语法适合批量增删,:style适合连续变化的单个数值(如百分比宽度);含连字符的 CSS 属性名要加引号或改驼峰。- 模板里的动态效果必须挂在响应式变量上,写死字面量(如
index === 0)等于放弃交互能力——这是 tab 高亮、购物车全选这类案例的通用套路。 v-model会按控件类型自动选value还是checked,所以 text/textarea/select 绑字符串、单个 checkbox 绑布尔、多个 checkbox 绑数组、radio 绑选中的value,选错类型就会“绑上了但不同步”。- computed 负责“算出值”,watch 负责“值变了去做事”:前者必须纯、必须
return;后者才允许发请求、写缓存、改别的数据。 - 遇到“数据改了页面不动”,排查顺序固定:属性是否在
data里预先声明 → 是否用:bind而非双大括号 → 数组/对象是否用了 Vue 检测得到的方式修改 →watch是否需要deep。