Skip to content

指令(下)、计算属性与侦听器(一) ​

本页对应 指令(下)、计算属性与侦听器,共 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="&#123;&#123; flag &#125;&#125;" 是错的。

对象语法:键是类名,值是布尔值 ​

为什么需要:一个类名需要来回切换时,用对象最直观。例如 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="&#123;&#123; width: ... &#125;&#125;"。

进度条案例:把宽度交给数据 ​

进度条最适合说明 :style 的价值:外层 .progress 固定宽度,内层 .inner 的宽度表示进度,文字也显示百分比。为什么需要:宽度是连续变化的,用行内样式绑定比写多个类名更直接。怎么想:data 里存一个 progress: 50,内层宽度写成 progress + '%',文字写成 &#123;&#123; progress + '%' &#125;&#125;;四个按钮点击时分别把 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 改成 &#123;&#123; progress + '%' &#125;&#125;,按钮分别加 @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,再按修饰符规则单独学习即可。

课件图示 ​

课件第5页:通过 "." 指明一些指令后缀,不同后缀封装了不同的处理操作 → 简化代码 ①按

课件第8页:语法:class = "对象/数组" ①对象→ 键就是类名,值是布尔值。如果值为

课件第10页:语法:style = "样式对象" v-bind 对于样式控制的增强- 操作st

课件第9页:案例:京东秒杀tab 导航高亮 核心思路: 1. 基于数据动态渲染tab → v

课件第12页:常见的表单元素都可以用v-model 绑定关联→ 快速获取或设置表单元素的值 v

课件第21页:总结 小结 业务技术点总结: 1. 渲染功能(不及格高亮) v-if v-els

课件第23页:watch 侦听器(监视器) 作用:监视数据变化,执行一些业务逻辑或异步操作。

课件第7页:v-bind 对于样式控制的增强 class 类名 style 行内样式 为了方

代码示例 ​

示例一:指令修饰符(按键 / 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="&#123;&#123; ... &#125;&#125;" 是错误写法。

示例三: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
7v-bind对class的控制(6-8)11:48
8案例-tab栏切换案例(9)07:01

小结 ​

  1. 修饰符是“指令的私有后缀”,位置永远紧跟在指令后面;跨指令使用一律无效,记不清就查官方文档对应指令那一节。
  2. 三类修饰符的归属要背下来:@keyup.enter/.esc/.键码 只给键盘事件;.stop/.prevent(可连写)只给事件指令;.trim/.number/.lazy 只给 v-model。
  3. .trim 解决“看不见的空格坑”,.number 只做类型转换、不做校验,.lazy 把“实时同步”改成“失焦同步”——三者都是收集时机/格式的微调,不是校验手段。
  4. :class 和 :style 的本质还是 v-bind:对象语法适合单个类名开关切换,数组语法适合批量增删,:style 适合连续变化的单个数值(如百分比宽度);含连字符的 CSS 属性名要加引号或改驼峰。
  5. 模板里的动态效果必须挂在响应式变量上,写死字面量(如 index === 0)等于放弃交互能力——这是 tab 高亮、购物车全选这类案例的通用套路。
  6. v-model 会按控件类型自动选 value 还是 checked,所以 text/textarea/select 绑字符串、单个 checkbox 绑布尔、多个 checkbox 绑数组、radio 绑选中的 value,选错类型就会“绑上了但不同步”。
  7. computed 负责“算出值”,watch 负责“值变了去做事”:前者必须纯、必须 return;后者才允许发请求、写缓存、改别的数据。
  8. 遇到“数据改了页面不动”,排查顺序固定:属性是否在 data 里预先声明 → 是否用 :bind 而非双大括号 → 数组/对象是否用了 Vue 检测得到的方式修改 → watch 是否需要 deep。