主题
生命周期与工程化开发(一)
本页对应 生命周期与工程化开发,共 7 段视频,约 1 小时 28 分。
学习目标
- 能使用
v-for渲染购物车商品列表,并正确绑定商品图片、单价、数量、小计及勾选状态。 - 能实现购物车商品的删除与数量加减,并在数量为 1 时禁用减号按钮。
- 能通过计算属性筛选已勾选商品,统计购物车总数量和总价。
- 能实现全选与反选:小复选框状态驱动全选框,全选框状态同步控制所有小复选框。
- 能使用
watch深度侦听购物车数据变化,并将数据持久化到本地存储,同时在初始化时优先读取本地数据。 - 能说明 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”先想到挂载阶段。

易错点: 不要把生命周期钩子写成对象,转写稿明确提醒:钩子要写成函数格式;写成对象就错了。
创建阶段:创建响应式数据,适合尽早发请求
为什么需要: 初始化渲染请求越早越好。创建阶段响应式数据已经建立,此时发请求,数据回来后更容易进入后续渲染流程。
怎么想: 转写稿回顾 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({...}) 的选项里写钩子函数。易错点:钩子要写成函数格式,不要写成对象格式;位置也不要放错。

四个阶段与八个钩子怎么对应
转写稿明确说:生命周期可以分成四个阶段——创建阶段、挂载阶段、更新阶段、卸载阶段;每个阶段对应两个函数,一共 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>课件图示








代码示例
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内的插值还是{{ count }}原文,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="{{}}"是无效的。 - 用
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 只能打开当前这一个项目,不要打开上层目录。
练习
钩子选择:一个页面需要(a)请求新闻列表、(b)让搜索框自动聚焦、(c)在数据变化后把列表写入
localStorage。分别应放在哪个钩子/选项中?说明理由,并写出三处代码骨架。图表时序:在小黑记账清单中,若把
echarts.init(document.getElementById('main'))写在created中会发生什么?请给出正确写法,并说明数据更新后如何让饼图跟着变。全选改造:现有
computed: { checkAll(){ return this.list.every(i => i.isChecked) } }被v-model绑定后报错。请改写为可读写形式,并说明set中需要做哪一步关键操作。持久化实现:为购物车
list增加本地存储能力,要求“有本地数据优先用本地,没有则用默认数组”,且任意一项的num/isChecked变化都要保存。写出data中list的初始化表达式与watch配置。找错题:下面的组件为什么渲染失败?指出至少两处问题并改正。
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 |
小结
- 生命周期是“创建→挂载→更新→销毁”四阶段,钩子是框架在特定阶段自动调用的函数,用来插入自己的代码。
- 实际开发只需牢记两个:
created发请求拿数据(越早越好),mounted做 DOM 操作(此时元素才存在)。 - 判断钩子归属的标准是“这段代码依赖什么”:依赖数据就放
created,依赖真实 DOM(含第三方库初始化)就放mounted。 - 钩子必须写成普通函数并与
data/methods/computed/watch平级,写错格式或层级会静默失效。 - 列表渲染 + 添加 + 删除 + 计算属性统计 + 图表,是同一套“数据驱动视图”的重复训练,核心永远是“改数据,不改 DOM”。
- 表单类交互优先用
v-model双向绑定;一旦绑到计算属性上,计算属性必须提供setter。 - 需要监听对象/数组内部变化时,
watch必须用对象写法并开启deep,配合JSON.stringify/parse完成本地持久化。 - 工程化阶段最容易踩的坑不在代码而在环境:终端路径必须等于项目根目录,否则启动的可能是另一个项目。