Skip to content

生命周期与工程化开发(一) ​

本页对应 生命周期与工程化开发,共 7 段视频,约 1 小时 28 分。

学习目标 ​

  1. 能使用 v-for 渲染购物车商品列表,并正确绑定商品图片、单价、数量、小计及勾选状态。
  2. 能实现购物车商品的删除与数量加减,并在数量为 1 时禁用减号按钮。
  3. 能通过计算属性筛选已勾选商品,统计购物车总数量和总价。
  4. 能实现全选与反选:小复选框状态驱动全选框,全选框状态同步控制所有小复选框。
  5. 能使用 watch 深度侦听购物车数据变化,并将数据持久化到本地存储,同时在初始化时优先读取本地数据。
  6. 能说明 Vue 实例生命周期的四个阶段及常用钩子函数,并区分 created 与 mounted 中适合执行的请求或 DOM 操作。

知识点 ​

今日目标与本文范围 ​

本节包含四块:生命周期、小黑记账清单、工程化开发入门、小兔仙首页。转写稿重点展开了购物车案例、脚手架启动、组件文件约束,以及生命周期中 created、mounted 两个钩子;小黑记账清单的饼图渲染、小兔仙的局部/全局注册属于课堂目标,本文只按转写稿说明目标,不虚构未讲步骤。学习时先抓“数据驱动视图”和“路径/组件约束”,再完成案例。

选项式API与生命周期钩子 ​

为什么需要:要知道代码写在哪、什么时候自动执行。转写稿中的“格瑞T”即 created,“mon记的”即 mounted。
怎么想:el 指定挂载盒子,data 存数据,methods 存方法,computed 放计算属性,watch 放侦听器;钩子要写成函数,不能写成对象。生命周期可分创建、挂载、更新、销毁四阶段,每阶段对应钩子,但先记两个最常用。
怎么做:

js
export default {
  data() { return { list: [] } },
  created() { /* 发Ajax请求 */ },
  mounted() { /* 找DOM元素、做DOM操作 */ },
  methods: {},
  computed: {},
  watch: {}
}

易错点:created 里不要找DOM;mounted 才适合DOM操作;钩子写成对象格式会报错。

脚手架启动:先对齐路径再运行 ​

为什么需要:转写稿反复强调“代码写了页面不刷新”,常见原因是启动的不是当前项目。
怎么想:终端路径必须等于项目根目录,再执行启动命令。
怎么做:进入项目文件夹后打开终端,确认路径,执行 npm run serve,浏览器访问 localhost:8080。若第三方包被删,先 npm i。
易错点:终端停在“03课堂代码”而项目在 vue-demo,直接运行会失败;VSCode 打开多一层目录也会报错;黑窗口不要用鼠标点,点了可能暂停,右键可继续。

组件文件约束与注册准备 ​

为什么需要:工程化后页面由组件组成,位置和命名错了会直接影响运行。
怎么想:App.vue 是根组件,放 src;自定义组件放 components;命名至少两个词,建议大驼峰。组件三部分:template、script、style。
怎么做:

vue
<template>
  <div>{{ uname }} {{ age }}</div>
</template>
<script>
export default {
  data() { return { uname: '张三', age: 18 } }
}
</script>
<style></style>

易错点:Vue2 的 template 只能有一个根元素;data 必须写成函数返回对象;组件名不要只用一个词。

购物车:渲染与选中背景 ​

为什么需要:列表要由数据循环出来,勾选状态要能双向变化。
怎么想:v-for 渲染,:key 用 id;背景类 active 由 item.isChecked 控制;复选框要用 v-model,不能用 :checked 单向绑定。转写稿调试时发现:改数据页面会变,但页面取消勾选数据不变,所以必须双向。
怎么做:

vue
<tr v-for="(item, index) in fruitList" :key="item.id"
    :class="{ active: item.isChecked }">
  <td><input type="checkbox" v-model="item.isChecked"></td>
  <td><img :src="item.icon"></td>
  <td>{{ item.price }}</td>
  <td>{{ item.number }}</td>
  <td>{{ item.price * item.number }}</td>
</tr>

易错点:active 不要按索引隔行加,应按是否勾选加;src、checked 是变量时要加冒号或改用 v-model。

购物车:删除与数量加减 ​

为什么需要:删除和修改数量都要定位到具体商品。
怎么想:删除用 filter 生成新数组;加减必须传索引或 id,否则不知道改哪一个;减到1时按钮禁用。
怎么做:

js
del(id) {
  this.fruitList = this.fruitList.filter(item => item.id !== id)
},
reduce(index) {
  const item = this.fruitList[index]
  if (item.number > 1) item.number--
},
add(index) {
  this.fruitList[index].number++
}
vue
<button :disabled="item.number === 1">-</button>

易错点:减法方法课堂中叫 reduce,与数组 reduce 同名但作用不同;disabled 要绑布尔值;减到0再减会出错。

购物车:统计与全选反选 ​

为什么需要:总价、总数是派生数据,用计算属性;全选要分两步做,否则容易乱。
怎么想:先 filter 出勾选商品,再 reduce 求和;全选第一步“点小框控大框”,用 every 判断是否全选中;第二步“点大框控小框”,因 v-model 会修改计算属性,计算属性要改成 get/set 对象写法。转写稿中的“I物”即 every,“铝算”即 return。
怎么做:

js
computed: {
  totalCount() {
    return this.fruitList.filter(i => i.isChecked)
      .reduce((sum, i) => sum + i.number, 0)
  },
  totalPrice() {
    return this.fruitList.filter(i => i.isChecked)
      .reduce((sum, i) => sum + i.price * i.number, 0)
  },
  check: {
    get() { return this.fruitList.every(i => i.isChecked) },
    set(val) { this.fruitList.forEach(i => { i.isChecked = val }) }
  }
}

易错点:计算属性默认只读,直接 v-model 会报错;条件不一定写 === true,能转布尔即可;全选要基于“筛选后的商品”统计,不是全部商品。

购物车:本地持久化 ​

为什么需要:刷新后数据不丢。
怎么想:初始化时优先读 localStorage,没有再用默认数据;数据任意属性变化都要保存,所以用 watch 深度侦听 fruitList。转写稿中的“真听器”即侦听器。
怎么做:

js
data() {
  return {
    fruitList: JSON.parse(localStorage.getItem('fruitList')) || [
      // 默认商品数据
    ]
  }
},
watch: {
  fruitList: {
    deep: true,
    handler(val) {
      localStorage.setItem('fruitList', JSON.stringify(val))
    }
  }
}

易错点:侦听器名字必须是 fruitList;对象属性变化要加 deep;存和取的 key 要一致;存对象/数组要先 JSON.stringify,取出来再 JSON.parse。

收尾:小黑记账清单与小兔仙首页 ​

按今日目标,小黑记账清单要练列表渲染、添加/删除、饼图渲染,并配合生命周期;小兔仙首页要练拆分模块、局部注册、结构样式完善、拆分组件与全局注册。转写稿明确这两块后续按课堂素材推进,本文不补写未讲的具体实现。做案例时优先把购物车这类“渲染—修改—统计—持久化”链路弄熟,再进入组件拆分。

先理解:生命周期到底解决什么问题 ​

为什么需要: 页面一打开,经常要做两类事:一是尽早发初始化请求拿数据;二是等页面结构出来后再操作 DOM。做早了,DOM 还不存在;做晚了,又浪费等待时间。生命周期就是告诉我们“在哪个阶段做什么事”。

怎么想: 转写稿里把生命周期类比成人的“从出生到死亡”:Vue 实例从 new Vue 开始,到实例销毁结束,整个过程就是生命周期。笔记把它分成四个阶段:创建、挂载、更新、销毁。每个阶段又对应钩子函数;转写稿提到一共 8 个函数,但本节先重点记两个:created 和 mounted。

怎么做: 先建立阶段图,不急着背全部钩子。看到“发请求”先想到创建阶段,看到“操作 DOM”先想到挂载阶段。

Vue生命周期四个阶段示意图

易错点: 不要把生命周期钩子写成对象,转写稿明确提醒:钩子要写成函数格式;写成对象就错了。

创建阶段:创建响应式数据,适合尽早发请求 ​

为什么需要: 初始化渲染请求越早越好。创建阶段响应式数据已经建立,此时发请求,数据回来后更容易进入后续渲染流程。

怎么想: 转写稿回顾 created 时说它“最先执行、自动执行”,里面可以发送 Ajax 请求。所以初始化列表、详情等请求,优先考虑放在 created。

怎么做:

js
export default {
  data() {
    return {
      list: []
    }
  },
  created() {
    // 实例创建后自动执行,适合尽早发送初始化请求
    // 发送请求,拿到数据后赋值给 this.list
  }
}

易错点: 不要在 created 里操作 DOM。转写稿反复强调:要操作 DOM,至少 DOM 得渲染出来;创建阶段还没挂载,找不到页面元素。

挂载阶段:渲染模板,适合操作 DOM ​

为什么需要: 有些效果必须等模板渲染到页面后才能做,比如找元素、初始化依赖 DOM 的插件。

怎么想: 笔记写“挂载阶段:渲染模板”。转写稿说 mounted 里可以进行 DOM 操作;想找 DOM 元素,就要在网页结构出来以后找。

怎么做:

js
export default {
  mounted() {
    // 模板已经挂载到页面,可以操作 DOM
    // document.querySelector(...)
  }
}

易错点: 不要把“发初始化请求”和“操作 DOM”混在一个时机里。请求越早越好放 created;DOM 操作至少等 mounted。另外转写稿提醒调试时先确认终端路径是项目根目录、启动的是当前项目,否则代码写了页面没反应,容易误判成生命周期没执行。

更新阶段:修改数据,更新视图 ​

为什么需要: 页面不是一成不变的,用户点击、输入、删除都会改数据,视图要跟着变。

怎么想: 笔记写“更新阶段:修改数据,更新视图”。生命周期理解到“数据变→视图更新”即可;本节重点不是背更新钩子,而是知道更新发生在挂载之后。

怎么做: 修改 data 中的响应式数据,模板使用该数据的地方会重新渲染。调试时可通过页面变化观察更新阶段是否发生。

易错点: 不要把所有“监听数据变化”都叫生命周期更新阶段。转写稿里 watch 是侦听器,和生命周期钩子是并列选项,别放错位置。

销毁阶段:销毁 Vue 实例 ​

为什么需要: 实例不用了要销毁,避免残留逻辑继续占用资源。

怎么想: 笔记写“销毁阶段:销毁 Vue 实例”。本节先理解阶段存在,具体清理逻辑等后续再展开;转写稿也说明 8 个钩子先记两个即可,不要一上来全背。

怎么做: 知道实例从创建到销毁是一条完整链路;写代码时先把 created、mounted 用对。

易错点: 不要因为钩子多就乱放。转写稿说“只记两个就行了”,先把 created 发请求、mounted 操作 DOM 这两个高频场景记牢。

钩子写法和本节小结 ​

为什么需要: 选项式 API 里 el、data、methods、computed、watch、生命周期钩子都是并列选项,放错位置代码就不生效。

怎么想: 转写稿回顾:created、mounted 是生命周期钩子函数,必须写成函数;methods 存方法,computed 放计算属性,watch 是侦听器。

怎么做:

js
export default {
  data() {
    return { list: [] }
  },
  methods: {},
  computed: {},
  watch: {},
  created() {
    // 尽早发初始化请求
  },
  mounted() {
    // DOM 渲染后操作
  }
}

易错点: 钩子写成对象、把 watch 当生命周期、在 created 里找 DOM,都是本节常见错误。记住一句话:请求尽早放 created,DOM 操作等 mounted;钩子必须是函数。

生命周期钩子要解决什么问题 ​

Vue 实例从 new Vue() 开始,到实例销毁,中间会经历创建、挂载、更新、卸载等阶段,这一整段过程就叫 Vue 生命周期。课上用“人的生命周期”作类比:从出生到死亡,每个阶段都有对应的事情发生。Vue 在生命周期过程中会自动运行一些函数,这些函数就叫【生命周期钩子】。它的价值是:让开发者可以在【特定阶段】运行自己的代码,比如创建后发请求、挂载后操作 DOM。

为什么需要:如果没有钩子,我们很难准确知道“数据准备好了没有”“模板渲染成真实 DOM 了没有”。怎么想:把钩子理解成 Vue 在特定时刻“通知你”的入口。怎么做:在 new Vue({...}) 的选项里写钩子函数。易错点:钩子要写成函数格式,不要写成对象格式;位置也不要放错。

Vue生命周期钩子示意图

四个阶段与八个钩子怎么对应 ​

转写稿明确说:生命周期可以分成四个阶段——创建阶段、挂载阶段、更新阶段、卸载阶段;每个阶段对应两个函数,一共 8 个钩子函数,但“不要紧张,只记两个就行了”。Vue2 中常见对应关系是:

  • 创建阶段:beforeCreate、created
  • 挂载阶段:beforeMount、mounted
  • 更新阶段:beforeUpdate、updated
  • 卸载阶段:beforeDestroy、destroyed

为什么需要:知道阶段,才能判断代码该放哪里。怎么想:先问“我现在要拿数据,还是要拿 DOM?”要发 Ajax 请求,放 created;要操作 DOM,放 mounted。怎么做:在选项里按函数写。易错点:不要把 created、mounted 写成对象;也不要以为 8 个都要背,先重点掌握 created 和 mounted。

代码骨架:四个阶段填在哪里 ​

本节笔记给出的骨架如下,四个注释就是填钩子的位置:

html
<div id="app">
    <h3>{{ title }}</h3>
    <div>
      <button @click="count--">-</button>
      <span>{{ count }}</span>
      <button @click="count++">+</button>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        count: 100,
        title: '计数器'
      },
      // 1. 创建阶段(准备数据)
     

      // 2. 挂载阶段(渲染模板)
      

      // 3. 更新阶段(修改数据 → 更新视图)
      

      // 4. 卸载阶段
     
    })
  </script>

为什么需要:骨架把“阶段”直观摆出来,避免把钩子写到 data、methods 里面。怎么想:创建阶段关注数据准备,挂载阶段关注模板渲染,更新阶段关注数据变化后的视图更新,卸载阶段关注收尾清理。怎么做:在对应注释下写 created() { ... }、mounted() { ... } 等函数。易错点:钩子名拼写要准确;更新阶段可能触发多次,不要在里面随意改数据造成反复更新。

重点记两个:created 和 mounted ​

课上反复强调:8 个钩子里先重点记两个。created 在创建阶段执行,此时数据已经准备好,适合发送 Ajax 请求;mounted 在挂载阶段执行,此时模板已经渲染成真实 DOM,适合做 DOM 操作。

为什么需要:这是最常用、也最容易混的两个。怎么想:转写稿里的调试思路是——“如果需要 DOM 操作,我就需要找 DOM 元素;想找到 DOM 元素,就要在 mounted 里边去找。”怎么做:把请求放 created,把找元素、初始化 DOM 相关操作放 mounted。易错点:在 created 里找 DOM 往往拿不到,因为还没挂载;这时应换到 mounted 再试。转写稿里“音频元素”“盗目操作”是转写同音字,实际应理解为“DOM 元素”“DOM 操作”。

易错点与调试顺序 ​

第一,钩子必须写成函数,不能写成对象,这是课上明确提醒的。第二,created 和 mounted 别混:要请求数据优先 created,要操作 DOM 优先 mounted。第三,调试时先打印或取元素验证阶段:在 created 里取不到 DOM,不要急着怀疑选择器,先看生命周期阶段对不对;换到 mounted 能取到,就说明阶段用对了。第四,更新阶段对应“修改数据 → 更新视图”,卸载阶段用于收尾。把这一节学完后,再回看计数器代码,就能理解:钩子不是手动调用的,而是 Vue 在特定阶段自动执行的入口。

先看生命周期钩子放在哪里 ​

选项式 API 里,el、data、methods、computed、watch 是并列选项;created、mounted 也是放在同一层,但它们是生命周期钩子函数,必须写成函数,不能写成对象。转写稿里反复强调:created 会“自动执行”,里面可以发送 Ajax 请求;mounted 也是自动执行,适合做 DOM 操作。
为什么需要:有些数据一进页面就要从接口拿;有些操作必须等真实 DOM 渲染出来才能做。
怎么想:created 时数据已经可用,但 DOM 还没挂载;mounted 时 DOM 已挂载,能拿到元素。
怎么做:发请求放 created,找元素、聚焦放 mounted。
易错点:写成 created: {};在 created 里找 DOM;把请求写到 methods 里却忘了调用。

案例一:在created中发送数据 ​

为什么需要:新闻列表的数据来自接口,不能把三条静态新闻写死。
怎么想:页面先给 list: [],在 created 里发 GET 请求,成功后把返回数据赋给 list,模板再用 v-for 渲染。转写稿说“这里面可以发送 Ajax 请求”,说的就是这个位置。
怎么做:保留原结构,图片 alt 改成中文;脚本中补 created。接口地址按笔记注释:http://hmajax.itheima.net/api/news,请求方式 GET。
易错点:list 不要初始成 null;请求成功后要确认返回层级再赋值;v-for 要加 :key;图片地址是变量时前面加 :。

html
<div id="app">
  <ul>
    <!-- 原笔记为静态 li,实际应改为 v-for 渲染 list -->
    <li class="news">
      <div class="left">
        <div class="title">5G商用在即,三大运营商营收持续下降</div>
        <div class="info">
          <span>新京报经济新闻</span>
          <span>2222-10-28 11:50:28</span>
        </div>
      </div>
      <div class="right">
        <img src="http://ajax-api.itheima.net/public/images/0.webp" alt="新闻配图">
      </div>
    </li>
  </ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const app = new Vue({
  el: '#app',
  data: {
    list: []
  },
  created() {
    axios.get('http://hmajax.itheima.net/api/news').then(res => {
      // 具体赋值层级以接口实际返回为准
      this.list = res.data
    })
  }
})
</script>

调试时先看 Network 里请求是否成功,再看 list 是否被赋值,最后检查模板有没有循环 list。

案例二:在mounted中获取焦点 ​

为什么需要:搜索框进入页面后要自动聚焦,方便用户直接输入。聚焦操作的是真实 DOM,created 阶段还拿不到,所以放到 mounted。
怎么想:笔记里输入框有 id="inp",mounted 执行时 DOM 已挂载,可以用 document.getElementById('inp') 找到它并调用 focus()。转写稿说“需要 DOM 操作,就需要在 mounted 里边去找”,就是这个思路。
怎么做:保留原 HTML 和图片,alt 改中文;在 Vue 实例中补 mounted。
易错点:在 created 里 focus() 会拿不到元素;id 写错;mounted 只执行一次,不要把它当成输入框每次输入都触发的钩子。

html
<div class="container" id="app">
  <div class="search-container">
    <img src="https://www.itheima.com/images/logo.png" alt="黑马程序员Logo">
    <div class="search-box">
      <input type="text" v-model="words" id="inp">
      <button>搜索一下</button>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const app = new Vue({
  el: '#app',
  data: {
    words: ''
  },
  mounted() {
    document.getElementById('inp').focus()
  }
})
</script>

如果聚焦没生效,先在控制台确认 document.getElementById('inp') 是否能拿到元素,再看 mounted 是否写成了函数格式。

两个钩子的选择口诀 ​

created 发请求,mounted 碰 DOM。 先判断你要做的是“拿数据”还是“操作页面元素”,再选钩子;两个钩子都自动执行,但不要为了用而用。

需求图示与本节目标 ​

小黑记账清单需求图示

本节做“小黑记账清单”:左侧是账单列表和添加表单,右侧是消费分类饼图。转写稿里明确说,这个案例一是继续练“渲染、添加、删除、统计”这些重复操作,二是练生命周期函数,饼图是相对新的点。因此不要一上来写全部功能,而应按“先列表、再交互、最后图表”的顺序推进。

需求分析与整体思路 ​

需求分四块:基本渲染、添加功能、删除功能、饼图渲染。为什么需要先拆需求?因为四块共用同一份账单数据:列表靠它渲染,统计靠它计算,添加删除会改它,饼图也要跟着它更新。怎么想?把数据当成唯一源头,页面和图表都从数据推导。怎么做?在 data 里准备账单数组,created 中发请求拿数据,模板用 v-for 渲染,统计用计算属性,添加删除后重新请求或更新数据,mounted 中初始化饼图。易错点:不要一边操作 DOM 一边又用 Vue 管数据;统计不要写成普通方法反复调用,应优先用 computed。

基本渲染:created 取数,v-for 渲染,computed 统计 ​

为什么需要 created?转写稿里说生命周期是“从 new Vue 开始到实例销毁”,created 会自动执行,适合发 Ajax 请求;此时 data 已经可用。怎么想?请求回来不要直接拼 HTML,而是存进响应式数据,让模板自动更新。怎么做?在 data 中先声明列表字段,created() 中请求接口并赋值;模板中 v-for 循环账单;消费总计用 computed 对列表价格求和。易错点:请求字段名要和接口文档一致;列表初始值要是数组;计算属性必须有 return;不要把统计结果写死成示例里的 298.00。

添加功能:v-model 收数据,非空判断后请求 ​

为什么需要表单收集?因为用户输入的消费名称和价格要进入同一份账单数据。怎么想?先用 v-model 做双向绑定,价格可用指令修饰符处理成合适类型;点击添加时先做非空判断,再发请求。怎么做?给两个输入框绑定 v-model,按钮注册点击事件;请求成功后清空输入框,并重新渲染列表。易错点:按钮在 form 中要写 type="button",否则可能触发表单默认提交导致刷新;清空应放在请求成功之后;价格不要以字符串直接参与计算。

删除功能:拿当前行 id,删除后同步刷新 ​

为什么需要 id?因为删除必须明确删哪一条,不能靠页面文字猜。怎么想?点击“删除”时把当前行 id 传进方法,再按 id 发删除请求。怎么做?模板中注册点击事件并传参,方法里调用删除接口,成功后重新获取列表。易错点:不要用 index 当唯一标识;删除后统计和饼图都要跟着变,所以数据更新后要统一刷新;删除前可加确认,避免误点。

饼图渲染:mounted 初始化,数据变化后更新 ​

为什么放 mounted?转写稿里说需要操作 DOM 时要在 mounted 里找元素,因为此时页面已挂载。怎么想?先 echarts.init(dom) 初始化,再根据账单数据组装饼图数据,调用更新配置。笔记里写 echarts.setOptions({...}),实际 ECharts 常用方法是 setOption,写代码时以官方 API 为准。怎么做?给图表容器 id="main",mounted 中初始化;数据变化后重新 setOption。易错点:容器必须有宽高;不要重复初始化同一个 DOM;饼图数据要和列表统计同源,否则会出现“列表删了图表没变”。

代码准备:先搭骨架,再按数据流补逻辑 ​

下面代码保留了本节笔记中的样式、结构、依赖和接口说明。data 先留空,是为了按上面的顺序逐步补:列表数据、created 请求、computed 统计、methods 添加删除、mounted 饼图。调试时建议按转写稿强调的顺序:先确认请求是否拿到数据,再看 v-for 是否渲染,接着测添加删除后统计是否变化,最后看 mounted 中饼图是否初始化成功。

html
<!-- CSS only -->
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
/>
<style>
  .red {
    color: red!important;
  }
  .search {
    width: 300px;
    margin: 20px 0;
  }
  .my-form {
    display: flex;
    margin: 20px 0;
  }
  .my-form input {
    flex: 1;
    margin-right: 20px;
  }
  .table > :not(:first-child) {
    border-top: none;
  }
  .contain {
    display: flex;
    padding: 10px;
  }
  .list-box {
    flex: 1;
    padding: 0 30px;
  }
  .list-box  a {
    text-decoration: none;
  }
  .echarts-box {
    width: 600px;
    height: 400px;
    padding: 30px;
    margin: 0 auto;
    border: 1px solid #ccc;
  }
  tfoot {
    font-weight: bold;
  }
  @media screen and (max-width: 1000px) {
    .contain {
      flex-wrap: wrap;
    }
    .list-box {
      width: 100%;
    }
    .echarts-box {
      margin-top: 30px;
    }
  }
</style>

<div id="app">
  <div class="contain">
    <!-- 左侧列表 -->
    <div class="list-box">
      <!-- 添加资产 -->
      <form class="my-form">
        <input type="text" class="form-control" placeholder="消费名称" />
        <input type="text" class="form-control" placeholder="消费价格" />
        <button type="button" class="btn btn-primary">添加账单</button>
      </form>

      <table class="table table-hover">
        <thead>
          <tr>
            <th>编号</th>
            <th>消费名称</th>
            <th>消费价格</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>1</td>
            <td>帽子</td>
            <td>99.00</td>
            <td><a href="javascript:;">删除</a></td>
          </tr>
          <tr>
            <td>2</td>
            <td>大衣</td>
            <td class="red">199.00</td>
            <td><a href="javascript:;">删除</a></td>
          </tr>
        </tbody>
        <tfoot>
          <tr>
            <td colspan="4">消费总计: 298.00</td>
          </tr>
        </tfoot>
      </table>
    </div>

    <!-- 右侧图表 -->
    <div class="echarts-box" id="main"></div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
  /**
   * 接口文档地址:
   * https://www.apifox.cn/apidoc/shared-24459455-ebb1-4fdc-8df8-0aff8dc317a8/api-53371058
   *
   * 功能需求:
   * 1. 基本渲染
   * 2. 添加功能
   * 3. 删除功能
   * 4. 饼图渲染
   */
  const app = new Vue({
    el: '#app',
    data: {
      
    },
  })
</script>

课件图示 ​

课件第6页:Vue生命周期:一个Vue实例从创建到销毁的整个过程。 生命周期四个阶段:①创建

课件第7页:Vue 生命周期函数(钩子函数) Vue生命周期过程中,会自动运行一些函数,被称

课件第8页:Vue 生命周期函数(钩子函数) before Mount before Upd

课件第9页:Vue 生命周期函数(钩子函数) before Create before Mo

课件第12页:小黑记账清单 功能需求: 1. 基本渲染 2. 添加功能 3. 删除功能 4.

课件第10页:Vue 生命周期钩子案例- 新闻列表& 输入框自动聚焦 created 数据准备

课件第15页:开发Vue 的两种方式: 1. 核心包传统开发模式:基于html / css /

课件第16页:工程化开发& 脚手架Vue CLI 基本介绍: Vue CLI 是Vue 官方提

代码示例 ​

1. Vue 生命周期钩子演示(计数器) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>生命周期钩子</title>
</head>
<body>
  <div id="app">
    <h3>{{ title }}</h3>
    <div>
      <button @click="count--">-</button>
      <span>{{ count }}</span>
      <button @click="count++">+</button>
    </div>
    <button @click="destroy">销毁实例</button>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        count: 100,
        title: '计数器'
      },

      // 1. 创建阶段(准备数据)
      beforeCreate() {
        console.log('beforeCreate → data 还没准备好:', this.count) // undefined
      },
      created() {
        console.log('created → 数据已可用,可以发送请求:', this.count) // 100
      },

      // 2. 挂载阶段(渲染模板)
      beforeMount() {
        console.log('beforeMount → 模板还没渲染到页面:', document.querySelector('#app span').innerHTML) // {{ count }}
      },
      mounted() {
        console.log('mounted → DOM 已渲染,可以操作 DOM:', document.querySelector('#app span').innerHTML) // 100
      },

      // 3. 更新阶段(修改数据 → 更新视图)
      beforeUpdate() {
        console.log('beforeUpdate → 视图更新前:', document.querySelector('#app span').innerHTML)
      },
      updated() {
        console.log('updated → 视图更新后:', document.querySelector('#app span').innerHTML)
      },

      // 4. 销毁阶段
      beforeDestroy() {
        console.log('beforeDestroy → 销毁前,适合做收尾工作')
      },
      destroyed() {
        console.log('destroyed → 实例已销毁')
      },

      methods: {
        destroy() {
          this.$destroy()
        }
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • 8 个钩子都是函数写法(created() {}),写成对象 created: {} 就错了,和 data、methods 是并列的选项。
  • beforeCreate 里 this.count 是 undefined,created 里才能拿到数据 → 发请求要在 created 里。
  • beforeMount 里 #app 内的插值还是 &#123;&#123; count &#125;&#125; 原文,mounted 里才是渲染后的真实内容 → 操作 DOM 必须在 mounted。
  • 点击 +/- 只触发 beforeUpdate → updated,不会重新走 created/mounted。
  • $destroy() 只销毁实例、解绑响应式,页面 DOM 不会被自动移除(要移除得配合 v-if),所以点完销毁按钮后再点 + 数字不再变化。
  • 打开浏览器控制台(F12 → Console)才能看到这些输出,别在页面上找。

2. created 中发送请求 + mounted 中获取焦点 ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>created 发请求 / mounted 获取焦点</title>
  <style>
    * { margin: 0; padding: 0; list-style: none; }
    body { padding: 20px; }

    .search-box { display: flex; width: 600px; margin: 0 auto 20px; }
    .search-box input {
      flex: 1;
      padding: 10px 16px;
      font-size: 16px;
      border: 2px solid #c4c7ce;
      border-radius: 10px 0 0 10px;
      outline: 0;
    }
    .search-box button {
      width: 112px;
      border: 0;
      border-radius: 0 10px 10px 0;
      background-color: #ad2a27;
      color: #fff;
      font-size: 17px;
      cursor: pointer;
    }

    .news {
      display: flex;
      height: 120px;
      width: 600px;
      margin: 0 auto;
      padding: 20px 0;
      cursor: pointer;
    }
    .news .left {
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding-right: 10px;
    }
    .news .left .title { font-size: 20px; }
    .news .left .info { color: #999999; }
    .news .left .info span { margin-right: 20px; }
    .news .right { width: 160px; height: 120px; }
    .news .right img { width: 100%; height: 100%; object-fit: cover; }
  </style>
</head>
<body>
  <div id="app">
    <!-- mounted 中获取焦点 -->
    <div class="search-box">
      <input type="text" v-model="words" ref="inp" placeholder="搜索一下">
      <button>搜索一下</button>
    </div>

    <!-- created 中请求回来的数据,v-for 渲染 -->
    <ul>
      <li class="news" v-for="item in list" :key="item.id">
        <div class="left">
          <div class="title">{{ item.title }}</div>
          <div class="info">
            <span>{{ item.source }}</span>
            <span>{{ item.time }}</span>
          </div>
        </div>
        <div class="right">
          <img :src="item.img" alt="">
        </div>
      </li>
    </ul>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    // 接口地址:http://hmajax.itheima.net/api/news
    // 请求方式:get
    const app = new Vue({
      el: '#app',
      data: {
        words: '',
        list: []
      },

      // 1. 创建阶段:数据已准备好,越早发请求越好
      async created() {
        const res = await axios.get('http://hmajax.itheima.net/api/news')
        // 按接口实际返回结构调整,常见结构为 { message, data: [...] }
        this.list = res.data.data
        console.log('请求到的数据:', this.list)
      },

      // 2. 挂载阶段:DOM 已经渲染出来,才能拿到输入框并聚焦
      mounted() {
        this.$refs.inp.focus()
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • created 里发请求 → 拿到数据直接赋值给 this.list,Vue 会自动把列表渲染出来,不用手动操作 DOM。
  • mounted 里 this.$refs.inp.focus():ref="inp" 是拿到 DOM 元素的方式;在 created 里写 this.$refs.inp 是 undefined,因为 DOM 还没渲染。
  • 数据渲染用 v-for="item in list" :key="item.id",key 不要用 index(增删时会错乱)。
  • 图片地址是变量,必须写 :src(v-bind:src),写成 src="&#123;&#123;&#125;&#125;" 是无效的。
  • 用 async/await 时方法要加 async;接口返回结构要按实际打印结果取层级,res.data.data 写错就渲染不出来。
  • 别忘了引入 axios 的 CDN,否则 axios is not defined。

3. 小黑记账清单(列表渲染 + 添加/删除 + 饼图 + 生命周期) ​

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>小黑记账清单</title>
  <!-- CSS only -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
  <style>
    .red { color: red !important; }
    .my-form { display: flex; margin: 20px 0; }
    .my-form input { flex: 1; margin-right: 20px; }
    .table > :not(:first-child) { border-top: none; }
    .contain { display: flex; padding: 10px; }
    .list-box { flex: 1; padding: 0 30px; }
    .list-box a { text-decoration: none; }
    .echarts-box {
      width: 600px;
      height: 400px;
      padding: 30px;
      margin: 0 auto;
      border: 1px solid #ccc;
    }
    tfoot { font-weight: bold; }
    @media screen and (max-width: 1000px) {
      .contain { flex-wrap: wrap; }
      .list-box { width: 100%; }
      .echarts-box { margin-top: 30px; }
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="contain">
      <!-- 左侧列表 -->
      <div class="list-box">
        <!-- 添加资产 -->
        <form class="my-form" @submit.prevent>
          <input type="text" class="form-control" placeholder="消费名称" v-model.trim="name">
          <input type="text" class="form-control" placeholder="消费价格" v-model.number="price">
          <button type="button" class="btn btn-primary" @click="add">添加账单</button>
        </form>

        <table class="table table-hover">
          <thead>
            <tr>
              <th>编号</th>
              <th>消费名称</th>
              <th>消费价格</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(item, index) in list" :key="item.id">
              <td>{{ index + 1 }}</td>
              <td>{{ item.name }}</td>
              <td :class="{ red: item.price > 100 }">{{ item.price.toFixed(2) }}</td>
              <td><a href="javascript:;" @click="del(item.id)">删除</a></td>
            </tr>
          </tbody>
          <tfoot>
            <tr>
              <td colspan="4">消费总计: {{ total.toFixed(2) }}</td>
            </tr>
          </tfoot>
        </table>
      </div>

      <!-- 右侧图表 -->
      <div class="echarts-box" ref="main"></div>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        name: '',      // 消费名称
        price: '',     // 消费价格
        list: [
          { id: 1, name: '帽子', price: 99 },
          { id: 2, name: '大衣', price: 199 }
        ]
      },

      // 计算属性:消费总计
      computed: {
        total() {
          return this.list.reduce((sum, item) => sum + item.price, 0)
        }
      },

      // 创建阶段:实际项目中在这里发请求获取账单列表
      // created() {
      //   axios.get('接口地址').then(res => { this.list = res.data.data })
      // }

      // 挂载阶段:DOM 渲染完成,初始化饼图
      mounted() {
        this.myChart = echarts.init(this.$refs.main)
        this.renderChart()
      },

      // 更新阶段:数据变化后重新渲染饼图
      updated() {
        this.renderChart()
      },

      // 销毁阶段:释放图表实例
      beforeDestroy() {
        this.myChart && this.myChart.dispose()
      },

      methods: {
        // 添加账单
        add() {
          if (!this.name || !this.price) {
            alert('消费名称和消费价格不能为空')
            return
          }
          // 实际项目:发送 POST 请求,成功后重新渲染列表
          this.list.push({
            id: Date.now(),
            name: this.name,
            price: this.price
          })
          // 清空输入框
          this.name = ''
          this.price = ''
        },

        // 删除账单
        del(id) {
          // 实际项目:根据 id 发送 DELETE 请求,成功后重新渲染
          this.list = this.list.filter(item => item.id !== id)
        },

        // 渲染/更新饼图
        renderChart() {
          this.myChart.setOption({
            title: { text: '消费记录', left: 'center' },
            tooltip: { trigger: 'item' },
            series: [
              {
                type: 'pie',
                radius: '60%',
                data: this.list.map(item => ({
                  name: item.name,
                  value: item.price
                }))
              }
            ]
          })
        }
      }
    })
  </script>
</body>
</html>

关键行与易错点

  • 饼图必须在 mounted 里初始化:echarts.init 需要容器有真实宽高,写在 created 里会报 Can't get DOM width or height。这里用 ref="main" + this.$refs.main 拿容器。
  • updated 里再次 setOption 实现图表随数据刷新;同一份配置重复 setOption 会覆盖,不需要先 clear。
  • echarts-box 的 width: 600px; height: 400px 不能少,容器没尺寸图表就是空白。
  • v-model.trim 去掉名称首尾空格,v-model.number 把价格转成数字——不加 .number 的话价格是字符串,sum + item.price 会变成字符串拼接,总计算错。
  • item.price.toFixed(2) 要求 price 是数字类型,所以初始数据里价格不要加引号。
  • 非空判断写在 add 方法里,return 一定要加,否则空内容也会被 push 进列表。
  • 删除用 filter 生成新数组再赋值(this.list = ...),直接改原数组下标的方式 Vue2 检测不到。
  • 表格里 <a href="javascript:;"> 是为了防止跳转;表单用 @submit.prevent 防止回车提交刷新页面。
  • 总计用计算属性而不是在模板里写复杂表达式,reduce 记得给初始值 0。

常见错误 ​

1. 在 created 里操作 DOM,拿到的是 null

  • 现象:this.$refs.box 或 document.querySelector 在 created 中返回 null,后续 .innerHTML / echarts.init() 报 “Cannot read properties of null”。
  • 原因:created 阶段只完成了数据初始化,模板尚未渲染成真实 DOM,元素还不存在。
  • 修法:把 DOM 操作整体挪到 mounted;数据请求仍留在 created(越早越好),两者职责分开。

2. 数据请求写在 mounted 且用 async/await 阻塞了渲染感知

  • 现象:页面先白屏/空列表一段时间,或列表闪一下才出现;有时 mounted 里拿到的数据在图表里不显示。
  • 原因:把“取数据”和“用数据渲染 DOM”混在同一钩子里,且未等数据回来就初始化图表。
  • 修法:created 发请求拿数据 → mounted 只做“DOM 已就绪”的事;图表初始化放在 mounted,数据更新后再调用 setOption(或用 $nextTick 保证 DOM 更新完毕)。

3. 用 v-model 绑定 computed 却仍写函数式(getter-only)

  • 现象:点击“全选”控制台报 “Computed property was assigned to but it has no setter”。
  • 原因:v-model 是双向绑定,页面变化会写回计算属性;函数式 computed 只有 getter,不可写。
  • 修法:改为对象写法 { get(){...}, set(val){...} },在 set 里把新值同步到源数据(如遍历列表改写 item.isChecked)。

4. watch 监听数组/对象内部属性变化不触发

  • 现象:勾选、加减数量后本地存储不更新,刷新回到旧数据。
  • 原因:默认 watch 只监听引用变化,list[0].num++ 不改变引用;且未加 deep。
  • 修法:写完整对象形式 { handler(val){...}, deep: true };存的时候用 JSON.stringify,取的时候 JSON.parse,并配 || 默认值 兜底。

5. 生命周期钩子写成对象/箭头函数导致 this 丢失

  • 现象:钩子里 this.list 是 undefined,或钩子根本不执行。
  • 原因:created: { ... } 不是函数不会执行;箭头函数没有自己的 this,指向外层而非 Vue 实例。
  • 修法:钩子一律写成普通函数,与 data、methods、computed、watch 平级并列,不要放错层级。

6. 脚手架终端路径与项目根目录不一致

  • 现象:npm run serve 报错或启动后改代码页面无变化。
  • 原因:终端停在上一级目录,启动的是另一个项目。
  • 修法:先 cd 进项目文件夹(侧边栏顶层必须是项目根目录),再 npm run serve;VS Code 只能打开当前这一个项目,不要打开上层目录。

练习 ​

  1. 钩子选择:一个页面需要(a)请求新闻列表、(b)让搜索框自动聚焦、(c)在数据变化后把列表写入 localStorage。分别应放在哪个钩子/选项中?说明理由,并写出三处代码骨架。

  2. 图表时序:在小黑记账清单中,若把 echarts.init(document.getElementById('main')) 写在 created 中会发生什么?请给出正确写法,并说明数据更新后如何让饼图跟着变。

  3. 全选改造:现有 computed: { checkAll(){ return this.list.every(i => i.isChecked) } } 被 v-model 绑定后报错。请改写为可读写形式,并说明 set 中需要做哪一步关键操作。

  4. 持久化实现:为购物车 list 增加本地存储能力,要求“有本地数据优先用本地,没有则用默认数组”,且任意一项的 num/isChecked 变化都要保存。写出 data 中 list 的初始化表达式与 watch 配置。

  5. 找错题:下面的组件为什么渲染失败?指出至少两处问题并改正。

js
export default {
  data: { msg: 'hi' },
  created: () => { this.msg = 'hello' },
  template: `<h3>{{ msg }}</h3><p>{{ msg }}</p>`
}

原视频对照 ​

#视频时长
1反馈和回顾31:20
2今日内容介绍03:50
3案例-购物车-渲染功能07:36
4案例-购物车-删除和修改数量06:53
5案例-购物车-统计总价和总数12:09
6案例-购物车-全选反选14:50
7案例-购物车-持久化到本地11:42

小结 ​

  1. 生命周期是“创建→挂载→更新→销毁”四阶段,钩子是框架在特定阶段自动调用的函数,用来插入自己的代码。
  2. 实际开发只需牢记两个:created 发请求拿数据(越早越好),mounted 做 DOM 操作(此时元素才存在)。
  3. 判断钩子归属的标准是“这段代码依赖什么”:依赖数据就放 created,依赖真实 DOM(含第三方库初始化)就放 mounted。
  4. 钩子必须写成普通函数并与 data/methods/computed/watch 平级,写错格式或层级会静默失效。
  5. 列表渲染 + 添加 + 删除 + 计算属性统计 + 图表,是同一套“数据驱动视图”的重复训练,核心永远是“改数据,不改 DOM”。
  6. 表单类交互优先用 v-model 双向绑定;一旦绑到计算属性上,计算属性必须提供 setter。
  7. 需要监听对象/数组内部变化时,watch 必须用对象写法并开启 deep,配合 JSON.stringify/parse 完成本地持久化。
  8. 工程化阶段最容易踩的坑不在代码而在环境:终端路径必须等于项目根目录,否则启动的可能是另一个项目。