Skip to content

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

本页对应 指令(下)、计算属性与侦听器,共 7 段视频,约 1 小时 4 分。

学习目标 ​

  • 能够使用 v-bind:style 对象语法为元素设置动态样式,并正确处理含连字符的 CSS 属性名。
  • 能够通过变量控制进度条内层宽度,并结合按钮事件完成不同百分比进度的切换。
  • 能够将 v-model 应用于文本框、单选按钮组、下拉框、单个复选框和复选框组,并正确绑定 value、checked 及数组数据。
  • 能够声明并使用计算属性,在模板中不加括号调用,并说明其缓存特性及与 methods 调用执行次数的区别。
  • 能够使用计算属性的 get/set 完整写法修改计算属性,并在 set 中根据修改结果更新依赖数据。
  • 能够完成成绩列表渲染,使用 v-for 展示数据、v-if 控制空状态,并通过动态 class 对低于 60 分的成绩设置样式。

知识点 ​

先看本节原始结构:代码准备 ​

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>

v-model 的总思路:按控件类型自动选 value 或 checked ​

为什么需要:常见表单元素都要“取值”和“设值”。如果每个控件都手写事件、读 DOM,代码会散。怎么想:v-model 会根据控件类型自动选取正确方法:输入框、文本域、下拉菜单关联 value;复选框、单选框关联 checked。怎么做:给控件加 v-model="数据名",再在 data 中准备对应类型的数据。易错点:不要把所有控件都当成字符串;单选框、复选框要看选中状态,不能只看显示文字。

输入框与文本域:绑定 value ​

为什么需要:姓名、自我描述要能随输入变化,也要能由数据反填。怎么想:input:text 和 textarea 都用 value,所以直接 v-model。怎么做:姓名写 v-model="name",文本域写 v-model="desc",data 中给 name: ''、desc: ''。调试时在页面输入“123”,数据会变;在调试工具里改 name,页面也会变,这就是双向。易错点:文本域不要写成 <textarea>&#123;&#123; desc &#125;&#125;</textarea> 再混用 v-model,直接用 v-model 即可。

单选按钮:name 分组,value 定值,两个 radio 绑同一个数据 ​

为什么需要:性别只能选一个,且提交给后台的是“男/女”这样的值。怎么想:同一组 radio 的 name 要相同,才能互斥;每个 radio 要有 value;两个 radio 都写同一个 v-model。怎么做:<input type="radio" name="gender" value="男" v-model="gender">男,另一个 value="女",data 中 gender: ''。点击女生,gender 变“女”;把数据改成“男”,页面选中男。易错点:value 要和数据候选值一致;页面上的“男/女”是给用户看的,真正收集的是 value,不要以为拿到的是标签文字。

下拉菜单:select 绑 value,option 给 value ​

为什么需要:城市下拉要能默认选中,也要能改数据。怎么想:select 的 value 关联选中 option 的 value。怎么做:<select v-model="city">,每个 option 写 value="北京" 等,data 中 city: '上海',页面默认选中上海。页面改选北京,数据变北京;数据改成天津,页面也跟着变。易错点:按本节要求给 option 设置 value,避免把显示文字和提交值混在一起。

复选框:单个用布尔,一组用数组 ​

为什么需要:单个“是否单身”只有选中/不选中;一组爱好可以多选。怎么想:单个复选框用布尔值控制 checked;一组复选框要用数组收集多个 value。怎么做:单个写 v-model="single",data 中 single: false;勾选变 true,取消变 false。一组写 v-model="hobbies",每个复选框给不同 value,data 中 hobbies: ['吃饭', '睡觉'],数组里包含哪个 value,哪个就选中。易错点:一组复选框不能只用一个布尔值;取消“睡觉”后,数组里应只剩“吃饭”。

关键改造片段 ​

html
姓名:<input type="text" v-model="name">
是否单身:<input type="checkbox" v-model="single">
性别:
<input type="radio" name="gender" value="男" v-model="gender">男
<input type="radio" name="gender" value="女" v-model="gender">女
所在城市:
<select v-model="city">
  <option value="北京">北京</option>
  <option value="上海">上海</option>
  <option value="成都">成都</option>
  <option value="南京">南京</option>
</select>
自我描述:<textarea v-model="desc"></textarea>
js
data: {
  name: '',
  single: false,
  gender: '',
  city: '上海',
  desc: ''
}

易错点小结 ​

单选框忘记 name 或 value;两个 radio 没有绑定同一个数据;下拉菜单忘记给 option 写 value;单个复选框误用数组;一组复选框误用布尔值;把页面显示文字当成收集值。记住:输入类看 value,选中类看 checked,一组多选看数组。

1. 为什么需要计算属性 ​

为什么需要:礼物清单里已经有 list 数据,页面上还要显示“礼物总数”。如果直接在模板里写 list[0].num + list[1].num + list[2].num,一旦增删数据或数量变化,表达式就要跟着改,复用性差。

怎么想:总数不是用户直接输入的,而是由现有数据“算出来”的新值;它依赖 list,list 变了就应自动重算。这正是 computed 的用途。

怎么做:把求和逻辑写进 computed,页面像使用普通属性一样使用它。

易错点:不要把总数写死在 data 里;计算属性函数必须 return 计算结果。

2. 概念与语法 ​

为什么需要:Vue 希望“数据变化,页面自动更新”。计算属性基于现有数据产生新属性,依赖的数据变化时会自动重新计算。

怎么想:computed 是配置项,和 data 同级;一个计算属性对应一个函数;函数返回计算后的值;模板中用 &#123;&#123; 计算属性名 &#125;&#125;。

怎么做:

js
computed: {
  total() {
    let sum = 0
    this.list.forEach(item => {
      sum += item.num
    })
    return sum
  }
}

易错点:计算属性虽然写成函数,但使用时是属性,模板中不能加小括号;不能和 data 中的属性同名;内部 this 指向 Vue 实例,所以用 this.list。

3. 注意点 ​

为什么需要:computed 写错位置或用法,页面不会报“语法错误”,但结果会不对。

怎么想:把计算属性理解成“只读属性”:声明在 computed 中,使用在模板中。

怎么做:记住五条:与 data 同级;函数写法但仍是属性;不能与 data 同名;用法和普通属性一样;内部 this 指向 Vue 实例。

易错点:写进 methods 或 data;模板写成 &#123;&#123; total() &#125;&#125;;忘记 return。

4. 案例:礼物清单求和 ​

比如我们可以使用计算属性实现下面这个业务场景:

礼物清单统计效果图

先保留笔记中的代码准备:

html
<style>
    table {
      border: 1px solid #000;
      text-align: center;
      width: 240px;
    }
    th,td {
      border: 1px solid #000;
    }
    h3 {
      position: relative;
    }
  </style>

<div id="app">
    <h3>小黑的礼物清单</h3>
    <table>
      <tr>
        <th>名字</th>
        <th>数量</th>
      </tr>
      <tr v-for="(item, index) in list" :key="item.id">
        <td>{{ item.name }}</td>
        <td>{{ item.num }}个</td>
      </tr>
    </table>

    <!-- 目标:统计求和,求得礼物总数 -->
    <p>礼物总数:? 个</p>
  </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: '篮球', num: 1 },
          { id: 2, name: '玩具', num: 2 },
          { id: 3, name: '铅笔', num: 5 },
        ]
      }
    })
  </script>

怎么做:在 data 后面加逗号,添加 computed,并把模板中的 ? 换成 &#123;&#123; total &#125;&#125;。

js
computed: {
  total() {
    let sum = 0
    this.list.forEach(item => {
      sum += item.num
    })
    return sum
  }
}
html
<p>礼物总数:{{ total }} 个</p>

调试过程:刷新页面后总数应为 1+2+5=8。在控制台把某个 num 调大,页面总数会自动变化;在 total 中写 console.log('计算属性执行了'),模板里多次使用 &#123;&#123; total &#125;&#125;,控制台只打印一次,说明计算属性有缓存。

易错点:forEach 中累加要写 sum += item.num;最后必须 return sum。

5. 与 methods 对比 ​

为什么需要:同样的求和也能用 methods 写,但两者执行时机不同。

怎么想:computed 有缓存:依赖不变时,多次使用只执行一次;methods 没有缓存:模板中调用几次就执行几次。

怎么做:computed 在模板中写 &#123;&#123; total &#125;&#125;;methods 在模板中写 &#123;&#123; test() &#125;&#125;。

易错点:computed 不加括号,methods 调用要加括号。

6. 需要修改计算属性时:完整写法 ​

为什么需要:计算属性默认只读,直接给它赋值会报错;有些场景需要反向修改。

怎么想:把计算属性写成对象,get 负责计算返回,set 接收修改后的值。

怎么做:例如用姓和名拼全名,修改全名时在 set(val) 中拆分并回写姓、名。

易错点:不需要修改时用函数写法即可;需要修改时再用 get/set;set 的形参是修改后的值。

一、computed计算属性:把数据加工成结果 ​

为什么需要: 页面里经常要展示“由现有数据算出来的结果”,例如礼物清单中把每条 num 加起来得到礼物总数。如果每次都在模板里临时算,逻辑会散、可读性差,也不方便复用。

怎么想: 把“计算过程”封装成一个计算属性。它依赖 data 中的原数据,原数据变了,计算结果应自动更新。转写稿里强调:计算属性是“基于现有数据计算出来的一个新的属性”,依赖项变化会自动重新计算。

怎么做: 写在 computed 配置项中,和 data、methods 平级;计算属性本质是一个函数,必须 return 计算结果;模板中像普通属性一样使用,不加小括号。例如:

js
computed: {
  totalCount () {
    let total = this.list.reduce((sum, item) => sum + item.num, 0)
    return total
  }
}

模板中写 &#123;&#123; totalCount &#125;&#125;,JS 中写 this.totalCount。

易错点: ① 忘记 return,计算结果为 undefined;② 模板中写成 &#123;&#123; totalCount() &#125;&#125;,这就变成调用方法而不是使用计算属性;③ 把 computed 写到 data 或 methods 里面,位置不对;④ 转写稿中提到的 reduce、forEach 等同音字要写准确,this.list 不要写成 list 漏掉 this。

二、methods方法:处理业务逻辑与事件 ​

为什么需要: 除了“算出一个值”,开发中还有大量业务动作,例如按钮点击、删除、提交、事件处理。这些更适合写成方法,由调用者决定何时执行。

怎么想: methods 是给 Vue 实例提供方法,方法要被调用才执行。模板中调用方法要加小括号:&#123;&#123; 方法名() &#125;&#125;;事件绑定写 @事件名="方法名" 或 @事件名="方法名()"。JS 中调用写 this.方法名()。

怎么做: 写在 methods 配置项中,和 data、computed 平级。转写稿中对比测试时,在 methods 里写 test() 返回同样的计算结果,模板里调用 test(),可以算出相同结果。

易错点: ① 方法名调用时漏写小括号,模板不会执行方法;② 把“计算属性”和“方法”的使用语法混用:计算属性不加括号,方法要加括号;③ 方法没有缓存,页面中调用几次就执行几次,不要误以为它和计算属性一样只算一次;④ 转写稿里的 methods 不要写成 mes 等简写。

三、计算属性的优势:缓存特性与代码对比 ​

为什么需要: 如果同一计算结果在页面中多次使用,每次都重新执行计算会浪费性能。计算属性有缓存特性:第一次计算后缓存结果,再次使用直接读缓存;依赖项变化才自动重新计算并再次缓存。methods 没有缓存,调用一次执行一次。

怎么想: 转写稿的调试过程是:在计算属性方法中输出“计算属性方法执行了”,页面中两次使用 totalCount,刷新后只输出一次;再在模板中多次使用,仍然只执行一次。换成 methods 的 test(),页面调用两次就输出两次。这说明“多次使用同一结果”时计算属性更有优势。

怎么做: 笔记中的对比例子如下,保留原代码:

html
<style>
    table {
      border: 1px solid #000;
      text-align: center;
      width: 300px;
    }
    th,td {
      border: 1px solid #000;
    }
    h3 {
      position: relative;
    }
    span {
      position: absolute;
      left: 145px;
      top: -4px;
      width: 16px;
      height: 16px;
      color: white;
      font-size: 12px;
      text-align: center;
      border-radius: 50%;
      background-color: #e63f32;
    }
  </style>

<div id="app">
    <h3>小黑的礼物清单🛒<span>?</span></h3>
    <table>
      <tr>
        <th>名字</th>
        <th>数量</th>
      </tr>
      <tr v-for="(item, index) in list" :key="item.id">
        <td>{{ item.name }}</td>
        <td>{{ item.num }}个</td>
      </tr>
    </table>

    <p>礼物总数:{{ totalCount }} 个</p>
  </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: '篮球', num: 3 },
          { id: 2, name: '玩具', num: 2 },
          { id: 3, name: '铅笔', num: 5 },
        ]
      },
      computed: {
        totalCount () {
          let total = this.list.reduce((sum, item) => sum + item.num, 0)
          return total
        }
      }
    })
  </script>

易错点: ① 认为 methods 也有缓存;② 在计算属性里修改原数据或产生副作用,计算属性应聚焦“算结果”;③ 依赖项变化后忘记它会自动重算,手动再赋值反而容易出错;④ 模板中计算属性误加括号。

四、总结:什么时候用computed,什么时候用methods ​

为什么需要: 两者都能算出结果,但定位不同,选错会让代码难维护。

怎么想: 转写稿和笔记的结论一致:computed 有缓存,methods 没有缓存;当一个结果依赖其他多个值时,推荐计算属性;当处理业务逻辑、事件处理函数时,推荐 methods。

怎么做: 礼物总数、购物车总价、成绩统计等“派生数据”用 computed;点击删除、提交表单、修改状态等“动作”用 methods。计算属性必须 return,模板中不加括号;方法调用要加括号。

易错点: 不要把事件处理函数写成计算属性;也不要把纯计算结果写成方法后在模板里反复调用,除非你明确不需要缓存。

为什么简写只能读不能改 ​

计算属性完整写法结构示意

为什么需要: 计算属性也是属性,页面里能像普通数据一样访问它,于是很容易产生一个疑问:既然能访问,能不能直接修改?笔记里明确说:计算属性默认的简写只能读取访问,不能“修改”。
怎么想: 简写形式本质上只提供了“计算并返回结果”的能力,相当于只写了 getter。你直接给计算属性赋值时,Vue 找不到对应的 setter,就会报错。
怎么做: 先保留笔记中的原始结构,观察简写写法:

html
 <div id="app">
    姓:<input type="text" v-model="firstName"> +
    名:<input type="text" v-model="lastName"> =
    <span></span><br><br> 
    <button>改名卡</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
 		firstName: '刘',
        lastName: '备'
      },
      computed: {

      },
      methods: {

      }
    })
  </script>

易错点: 不要把“能读取计算属性”误解成“能随意赋值”。简写适合只读场景;一旦涉及修改,就要换完整写法。

完整写法的 get 与 set ​

为什么需要: 有些业务确实需要“改计算属性”,例如把“姓 + 名”拼成完整姓名后,用户直接修改完整姓名,再反推更新姓和名。
怎么想: 完整写法要把计算属性写成对象,里面提供 get 和 set。get 负责计算并返回结果;set 在计算属性被直接修改时执行,形参表示修改后的结果。
怎么做: 语法结构是:

js
computed: {
  fullName: {
    get() {
      return this.firstName + '.' + this.lastName
    },
    set(val) {
      const arr = val.split('.')
      this.firstName = arr[0]
      this.lastName = arr[1]
    }
  }
}

页面中把展示位置改成可修改的输入框,用 v-model 绑定 fullName,就能触发 set:

html
<div id="app">
  姓:<input type="text" v-model="firstName"> +
  名:<input type="text" v-model="lastName"> =
  <input type="text" v-model="fullName"><br><br>
  <button>改名卡</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      firstName: '刘',
      lastName: '备'
    },
    computed: {
      fullName: {
        get() {
          return this.firstName + '.' + this.lastName
        },
        set(val) {
          const arr = val.split('.')
          this.firstName = arr[0]
          this.lastName = arr[1]
        }
      }
    },
    methods: {}
  })
</script>

易错点: get 必须返回计算结果;set 的形参不要漏写。转写稿中反复强调:get 和以前简写函数一样,负责算结果;set 只在“直接修改计算属性”时执行。

调试过程:从报错到可修改 ​

为什么需要: 只看语法容易忘,必须通过“先改、报错、再补 set”的过程理解完整写法存在的意义。
怎么想: 先用 v-model="fullName" 去改计算属性,控制台会提示没有 setter;补上对象写法和 set 后,再改输入框,set 就会执行。
怎么做: 在 set(val) 中先打印 val,输入“尼古拉斯.赵四”后失焦,可以看到形参就是修改后的完整值;再用 split('.') 拆成数组,分别赋给 firstName 和 lastName。这样页面上的姓、名也会同步变化。
易错点: 分隔符要统一。get 里用点拼接,set 里就按点拆分;如果输入中没有分隔符,arr[1] 可能是 undefined,实际案例要加判空或约定格式。不要写成 get 用点、set 用空格,否则拆分结果会乱。

案例目标与整体思路 ​

成绩案例功能示意图

本案例把前面学过的 v-for、动态 class、v-on、v-model 修饰符和 computed 串起来,完成成绩列表的渲染、删除、添加以及总分/平均分统计。

为什么需要:单个知识点会写,不代表能组合成完整页面;综合案例训练的是“数据驱动视图”的完整链路。
怎么想:先让列表按数据渲染出来,再给每行加删除,再用表单收集新数据,最后用计算属性根据 list 自动算总分和平均分。
怎么做:模板负责展示和绑定事件,methods 负责增删,computed 负责统计。
易错点:不要一边渲染一边手动改 DOM;删除链接要阻止默认跳转;添加前要判空;计算属性在模板中不要加小括号。

渲染成绩列表 ​

为什么需要:成绩数据在 list 中,页面要用表格逐行展示,并且低于 60 分显示红色。
怎么想:用 v-for 循环 <tr>,用 :key 绑定唯一 id;分数单元格用动态 class,条件为 item.score < 60。转写稿中调试时把 20 分加到 60,红色消失;减到 59,又变红,说明动态类名由分数条件控制。
怎么做:

html
<tr v-for="(item, index) in list" :key="item.id">
  <td>{{ index + 1 }}</td>
  <td>{{ item.subject }}</td>
  <td :class="{ red: item.score < 60 }">{{ item.score }}</td>
  <td><a href="#" @click.prevent="del(item.id)">删除</a></td>
</tr>
<tr v-if="list.length === 0">
  <td colspan="4">暂无数据</td>
</tr>

易错点::class 的对象键 red 对应样式类名;条件要返回布尔值。暂无数据和列表行要二选一,转写稿用 v-if="list.length === 0" 控制“暂无数据”,删空后它才出现。:key 优先用数据已有 id,不要随便用重复值。

删除成绩 ​

为什么需要:用户需要移除某条成绩,删除后列表和统计结果都应自动变化。
怎么想:删除链接是 <a href="#">,点击会默认跳转,所以要用 v-on 绑定点击并阻止默认行为。笔记明确写“v-on 绑定事件,阻止 a 标签的默认行为”。
怎么做:模板写 @click.prevent="del(item.id)",方法中按 id 过滤:

js
methods: {
  del(id) {
    this.list = this.list.filter(item => item.id !== id)
  }
}

易错点:忘记 .prevent 会导致地址栏出现 # 或页面跳动;删除后如果 list 为空,“暂无数据”应自动显示;不要直接操作 DOM 删除行。

添加成绩 ​

为什么需要:案例要能录入新成绩,并把输入内容加入列表。
怎么想:用 v-model 双向绑定表单;科目用 .trim 去掉首尾空格,分数用 .number 转数字;添加前判断数据是否为空,添加后清空文本框。
怎么做:

html
<input v-model.trim="form.subject" placeholder="科目">
<input v-model.number="form.score" placeholder="分数">
<button @click="add">添加</button>
js
data() {
  return {
    list: [],
    form: { subject: '', score: '' }
  }
},
methods: {
  add() {
    if (!this.form.subject || this.form.score === '') return
    this.list.push({
      id: Date.now(),
      subject: this.form.subject,
      score: Number(this.form.score)
    })
    this.form.subject = ''
    this.form.score = ''
  }
}

易错点:.number 不是万能判空,空字符串仍要判断;添加后忘记清空输入框;新增数据要补唯一 id,避免 :key 重复。

统计总分与平均分 ​

为什么需要:总分和平均分依赖 list,增删后要自动重算,不能每次手动改。转写稿强调计算属性有缓存:多次使用只执行一次;而 methods 是调用一次执行一次。
怎么想:在 computed 中基于 list 累加分数,再除以长度得到平均分。
怎么做:

js
computed: {
  total() {
    let sum = 0
    this.list.forEach(item => {
      sum += Number(item.score)
    })
    return sum
  },
  average() {
    return this.list.length ? (this.total / this.list.length).toFixed(2) : 0
  }
}

模板中用 &#123;&#123; total &#125;&#125;、&#123;&#123; average &#125;&#125;。易错点:计算属性在模板中不能加小括号,加了就变成调用方法;空数组求平均要避免除以 0;分数若是字符串要先转数字。

课件图示 ​

课件第18页:计算属性完整写法 computed: { 计算属性名: { get() { 一段

![课件第14页:计算属性 data: { list: { id: 1, name: '篮球'

课件第16页:computed 计算属性vs methods 方法 computed 计算属性

课件第28页:综合案例- 水果购物车 需求说明: 1. 渲染功能 2. 删除功能 3. 修改个

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

课件第25页:watch 侦听器(监视器) ②完整写法→ 添加额外配置项 (1) deep:

课件第26页:总结 小结 watch侦听器的语法有几种? ①简单写法→ 监视简单类型的变化 ②

课件第29页:总结 总结 业务技术点总结: 1. 渲染功能:v-if/v-else v-for

代码示例 ​

示例 1: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 type="text" v-model="username">
      <br><br>
    是否单身:
      <input type="checkbox" v-model="isSingle">
      <br><br>
    性别:
      <input type="radio" name="sex" value="男" v-model="gender">男
      <input type="radio" name="sex" value="女" v-model="gender">女
      <br><br>
    所在城市:
      <select v-model="city">
        <option value="北京">北京</option>
        <option value="上海">上海</option>
        <option value="成都">成都</option>
        <option value="南京">南京</option>
      </select>
      <br><br>
    自我描述:
      <textarea v-model="desc"></textarea>

    <button @click="register">立即注册</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: true,      // 单个复选框 → 布尔值
        gender: '男',        // 单选框 → 与某个 value 相同
        city: '上海',        // 下拉框 → 与选中 option 的 value 相同
        desc: '一个爱学习的前端'
      },
      methods: {
        register () {
          console.log({
            username: this.username,
            isSingle: this.isSingle,
            gender: this.gender,
            city: this.city,
            desc: this.desc
          })
        }
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • v-model 会根据控件类型自动选正确的方法:文本框、文本域、下拉框绑的是 value,复选框、单选框绑的是 checked。
  • 单选框必须同时具备 name(同组互斥)和 value(提交给后台的数据),v-model 绑定的数据值要和 value 之一保持一致,否则一个都不选中。
  • 拿到的是 value,不是页面上给用户看的文字:男、女神 这种显示文字与数据无关。
  • 单个复选框绑布尔值,多个复选框组成一组时必须绑数组。
  • select 的 v-model 写在 <select> 上,不是写在 <option> 上;option 要写 value,否则提交的是标签文字。

示例 2:computed 计算属性 vs methods 方法(缓存差异)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>computed vs methods</title>
  <style>
    table {
      border: 1px solid #000;
      text-align: center;
      width: 300px;
    }
    th, td {
      border: 1px solid #000;
    }
    h3 {
      position: relative;
    }
    span {
      position: absolute;
      left: 145px;
      top: -4px;
      width: 16px;
      height: 16px;
      color: white;
      font-size: 12px;
      text-align: center;
      border-radius: 50%;
      background-color: #e63f32;
    }
  </style>
</head>
<body>
  <div id="app">
    <h3>小黑的礼物清单🛒<span>{{ totalCount }}</span></h3>
    <table>
      <tr>
        <th>名字</th>
        <th>数量</th>
      </tr>
      <tr v-for="(item, index) in list" :key="item.id">
        <td>{{ item.name }}</td>
        <td>{{ item.num }}个</td>
      </tr>
    </table>

    <p>礼物总数(computed):{{ totalCount }} 个</p>
    <p>礼物总数(computed 第二次用):{{ totalCount }} 个</p>
    <p>礼物总数(methods):{{ getTotalCount() }} 个</p>
    <p>礼物总数(methods 第二次调):{{ getTotalCount() }} 个</p>
  </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: '篮球', num: 3 },
          { id: 2, name: '玩具', num: 2 },
          { id: 3, name: '铅笔', num: 5 }
        ]
      },
      computed: {
        totalCount () {
          console.log('计算属性执行了')
          let total = this.list.reduce((sum, item) => sum + item.num, 0)
          return total
        }
      },
      methods: {
        getTotalCount () {
          console.log('方法执行了')
          let total = this.list.reduce((sum, item) => sum + item.num, 0)
          return total
        }
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • computed 与 data、methods 是同级配置项,不要写到 data 里面去。
  • 计算属性是函数写法、属性用法:模板中写 &#123;&#123; totalCount &#125;&#125;,不能加小括号,加了就变成调用方法了;JS 中用 this.totalCount。
  • 打开控制台可见:页面里用了两次 totalCount,但"计算属性执行了"只打印一次——第一次算完就缓存,依赖项(list 里的 num)变了才重新计算。
  • methods 每调用一次执行一次,没有缓存,所以模板里写 &#123;&#123; getTotalCount() &#125;&#125; 必须带括号。
  • 计算属性内部 this 依然指向 Vue 实例;计算属性名不能和 data 中的属性同名。
  • 结论:结果依赖多个数据 → 用 computed;处理业务逻辑(事件处理函数)→ 用 methods。

示例 3:计算属性的完整写法(get / set)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>计算属性完整写法</title>
</head>
<body>
  <div id="app">
    姓:<input type="text" v-model="firstName"> +
    名:<input type="text" v-model="lastName"> =
    <span>{{ fullName }}</span><br><br>

    全名(可修改):<input type="text" v-model.lazy="fullName"><br><br>

    <button @click="changeName">改名卡</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        firstName: '刘',
        lastName: '备'
      },
      computed: {
        fullName: {
          // get:读取 fullName 时执行,和原来的简写函数一模一样
          get () {
            return this.firstName + ' ' + this.lastName
          },
          // set:直接修改 fullName 时执行,形参 value 是修改后的结果
          set (value) {
            console.log('set 收到:', value)
            const arr = value.split(' ')
            this.firstName = arr[0] || ''
            this.lastName = arr[1] || ''
          }
        }
      },
      methods: {
        changeName () {
          // 直接"修改"计算属性,会触发上面的 set
          this.fullName = '诸葛 亮'
        }
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • 简写(只写一个函数)只能读取,不能修改;一旦用 v-model 绑到计算属性上再输入内容,就会报 Computed property "fullName" was assigned to but it has no setter 错误。
  • 完整写法是对象格式:计算属性名: { get(){}, set(){} },有多个计算属性时继续逗号往后写,每个都是这个对象结构。
  • get 里必须 return 计算结果;set 的形参就是修改后的新值,一般要在里面反向去改它所依赖的 data(这里是 firstName、lastName)。
  • 用 v-model.lazy 是为了失焦/回车时才触发,否则每敲一个字符就进一次 set,体验和数据都乱。
  • 易错:split 出来的第二项可能是 undefined,所以要 || '' 兜底;set 里写回 data 时注意别把 this 弄丢。
  • 用不到修改就用简写函数,需要"被修改"才写 get/set 完整写法。

常见错误 ​

  1. 单个复选框勾选后数据没变化,或一组复选框勾一个全勾上

    • 现象:<input type="checkbox" v-model="isAgree"> 点击无反应;三个"爱好"复选框勾任意一个,另外两个同时被选中。
    • 原因:v-model 绑定的数据类型与控制类型不匹配。单个 checkbox 需要布尔值,一组 checkbox 必须绑定数组,且每个 checkbox 要写 value。
    • 修法:单个用 data: { isAgree: false };一组用 hobby: [],每个 input 加 value="eat" 之类的值。
  2. 单选框选中后拿到 null / "on",或初始值设了却不选中

    • 现象:点"男",sex 变成 "on" 或 null;data 里写了 sex: '男',页面两个都不选中。
    • 原因:没写 value(浏览器默认 value 是 "on");或 v-model 初始值与任何一个 radio 的 value 都不严格相等(含类型、空格差异);或同组没写相同 name 导致不互斥。
    • 修法:每个 radio 显式写 value,同一组共用同一个 v-model 变量,初始值必须是某个 value 的原样拷贝。
  3. 下拉框选中后取到 undefined,或初始不选中

    • 现象:选"北京",address 是 undefined;设了 address: '北京' 页面却是空选。
    • 原因:<option> 没写 value;或 v-model 初始值与 option 的 value 类型不一致(数字 1 与字符串 "1")。
    • 修法:每个 option 显式写 value,v-model 的初始值与之一一对应、类型一致。
  4. 计算属性忘记 return,页面显示 undefined

    • 现象:&#123;&#123; total &#125;&#125; 渲染为空白或 undefined,控制台无报错。
    • 原因:函数体用了 {} 却没有 return;或改写成箭头函数导致 this 不是 Vue 实例,this.list 为 undefined。
    • 修法:计算属性必须 return 结果;保持 total() { ... } 普通函数写法,别用箭头函数。
  5. 直接给计算属性赋值报错

    • 现象:输入框改不动,控制台报 Computed property "fullName" was assigned to but it has no setter。
    • 原因:计算属性简写形式只有 getter,是只读的。
    • 修法:改成对象写法 { get(){ return ... }, set(val){ ... } },在 set 里把 val 拆解回它依赖的 data。
  6. computed 与 data 同名,或在 computed 里做异步

    • 现象:报 The computed property "xxx" is already defined in data;或模板渲染出 [object Promise]。
    • 原因:computed 与 data 是同级配置项,名字不能重复;计算属性必须同步返回结果。
    • 修法:改名;异步取数放 created 或 methods,拿到结果后再赋值给 data,computed 只做同步派生。
  7. 模板中 computed 加了括号、methods 忘了括号

    • 现象:&#123;&#123; totalCount() &#125;&#125; 报 totalCount is not a function;&#123;&#123; getTotal &#125;&#125; 直接把函数体打印到页面。
    • 原因:computed 是属性,methods 是方法,调用形式不同。
    • 修法:computed 不加括号,methods 在插值里必须加括号(事件绑定写方法名除外)。
  8. :style 对象里属性名不加引号、数值不带单位

    • 现象:宽度设置不生效,或控制台报语法错误。
    • 原因:{ width: 100 } 生成无单位的 width: 100,浏览器直接丢弃;background-color 含中划线,不是合法标识符。
    • 修法:写成 { width: '100px' }、{ 'background-color': 'red' } 或驼峰 backgroundColor。

练习 ​

  1. 表单全收集:把笔记里的注册页补全,用 v-model 收集姓名、是否单身、性别、所在城市、自我描述五个字段,再加一组"爱好"多选(吃饭/睡觉/打豆豆)。点按钮把整个对象 console.log 出来。

    • 验收:手动改 data 里任一字段,页面同步变化;反向在页面操作,data 同步变化;爱好的值是数组,且数组中存的是 value 而不是页面文字。
  2. 进度条:用两层 div 搭进度条,内层宽度只通过 :style 对象绑定一个变量 percent 控制,不新增任何 class;四个按钮分别设 25/50/75/100,加 transition 让宽度平滑过渡。

    • 验收:按钮点击后宽度变化且有动画;全程只改一个变量,没有出现 .w25、.w50 这类类名。
  3. computed vs methods 实测:礼物清单求总数,先用 methods 写一遍,再用 computed 写一遍,两处都加 console.log,页面上各调用三次。

    • 验收:刷新后 computed 的日志只出现一次,methods 的日志出现三次;改任一 num 后,computed 重新计算且只算一次。
  4. 计算属性完整写法:姓 + 名 → 全名,用 v-model 绑定全名输入框,修改全名后能拆回姓和名(用 split(' ') 处理)。

    • 验收:不写 set 时修改全名控制台报无 setter 错误;补上 get/set 后修改全名不再报错,姓和名输入框同步更新。
  5. 成绩案例:渲染成绩列表,低于 60 分显示红色(:class 对象写法);列表为空时显示"暂无数据";用 computed 求总分与平均分。

    • 验收:把某个分数改成 60 时红色消失、改成 59 时变红;删空列表后只剩"暂无数据"一行;平均分保留两位小数,且列表为空时不出现 NaN。

原视频对照 ​

#视频时长
1v-bind对style的控制(10)03:36
2案例-修改进度条案例(10)07:02
3v-model应用于其他表单项(11-12)17:08
4计算属性-基本语法(13-14)10:31
5计算属性-和methods方法对比(15-16)06:04
6计算属性-完整写法(17-18)13:43
7案例-成绩列表渲染(19-21)06:13

小结 ​

  1. v-model 的绑定类型由控件类型决定,不由你的喜好决定:文本类控件(input:text / textarea / select)绑字符串或数字,单个 checkbox 绑布尔,一组 checkbox 绑数组,radio 绑字符串。
  2. 页面上显示的文字和程序拿到的值可以完全无关:前者是给用户看的,后者是 value。一组控件的 value 必须逐一显式书写。
  3. 计算属性是属性而非方法:写在 computed 配置项、与 data 同级、模板中不加括号、不能与 data 同名、必须有 return、内部 this 指向实例。
  4. computed 的缓存以"依赖是否变化"为失效条件,与"被使用多少次"无关;methods 每次渲染都重新执行,这是二者唯一的性能分水岭。
  5. 选型口诀:一个结果由多个数据推导而来 → computed;一次动作的处理逻辑(点击、提交) → methods。
  6. 计算属性默认只读,要让它被 v-model 反向写入就必须写 get/set;set 的职责不是赋值给自身,而是把新值拆解回它依赖的源数据。
  7. 依赖必须是响应式的(data / props / 其他 computed),依赖非响应式数据时计算属性不会重新计算,页面会"卡住不动"。
  8. 针对连续变化的值(进度条百分比、条件类名),用 :style 对象或 :class 对象比预先堆一百个类名更合适;带中划线的 CSS 属性名要加引号,值要带单位。