主题
todoList与在脚手架中使用axios(Lesson8 2/3)
本页对应 Lesson08,共 6 段视频,约 1 小时 15 分。
学习目标
- 能将 TodoList 拆分为 Header、Main、Footer 等组件,并正确迁移公共样式与组件私有样式。
- 能实现任务添加功能,包括输入校验、重复标题提示、添加成功后清空输入框。
- 能根据任务 ID 修改选中状态,并实现全选、取消全选、已选数量与全部数量统计。
- 能根据任务 ID 删除指定任务,并实现鼠标移入显示删除按钮、清除已选任务等底部操作。
- 能使用 axios 获取任务数据并渲染到页面,同时处理加载中状态与响应体数据提取。
- 能使用 watch/updated 与 localStorage 实现任务数据持久化,并在页面刷新后恢复数据与选中统计。
知识点
先拆组件:App 只做容器
为什么需要:TodoList 由头部输入、主体列表、底部统计三块组成。如果全写在 App 里,模板和样式会越来越乱;而且头部、主体、底部是兄弟关系,数据不能只在某一个子组件里定义。
怎么想:把 App 当成共同父级,todos 定义在 App 中,三个子组件通过 props 接收数据,通过自定义事件通知 App 修改数据。这样“数据源唯一”,兄弟组件也能共享状态。
怎么做:在 src/components 下建 TodoHeader.vue、TodoMain.vue、TodoFooter.vue,App 中引入并使用:
vue
<template>
<div id="app">
<TodoHeader :todos="todos" @add="addTodo" />
<TodoMain :todos="todos" @update="updateTodo" @del="delTodo" />
<TodoFooter :todos="todos" @toggle-all="toggleAll" @clear-done="clearDone" />
</div>
</template>样式上要注意:scoped 只作用当前组件,子组件内部不一定生效。按钮等公共样式放 src/assets/css/base.css,在 App 或入口中引入:
js
import '@/assets/css/base.css'易错点:不要把 todos 定义在头部或主体里;不要把子组件样式全留在 App 的 scoped 中;组件名、事件名要统一。
完成添加:判空、判重、清空输入
为什么需要:输入框回车后要把任务加入 todos,但空标题和重复标题不应加入。
怎么想:输入框在 TodoHeader 内,数据在 App 中,所以子组件触发 add,父组件真正修改数组。
怎么做:
vue
<!-- TodoHeader -->
<input v-model="title" @keyup.enter="add" placeholder="请输入任务标题" />js
methods: {
add() {
const text = this.title.trim()
if (!text) return alert('请输入任务标题')
this.$emit('add', text)
this.title = ''
}
}js
// App
addTodo(title) {
const has = this.todos.some(item => item.title === title)
if (has) return alert('任务标题不允许重复')
this.todos.push({ id: Date.now(), title, isDone: false })
}易错点:先 trim() 再判空;判重用 some;添加成功后清空输入框;id 要唯一。
渲染列表与修改选中状态
为什么需要:主体要显示任务,并能勾选完成状态。
怎么想:TodoMain 只负责展示和发事件,不直接改父组件数据。勾选时把 id 传给 App,由 App 更新对应任务。
怎么做:
vue
<!-- TodoMain -->
<li v-for="item in todos" :key="item.id">
<input type="checkbox" :checked="item.isDone" @change="update(item.id)" />
<span>{{ item.title }}</span>
</li>js
// App
updateTodo(id) {
this.todos = this.todos.map(item =>
item.id === id ? { ...item, isDone: !item.isDone } : item
)
}易错点:不要直接改 props;更新数组时建议返回新数组;调试时可在 App 中打印 todos,确认 isDone 是否变化。
删除任务与移入显示按钮
为什么需要:每行删除按钮默认隐藏,鼠标移入当前行才显示。
怎么想:用一个 activeId 记录当前移入行的 id,初值设为 -1,避免和真实 id 相同。
怎么做:
vue
<li @mouseenter="activeId = item.id" @mouseleave="activeId = -1">
<button v-show="activeId === item.id" @click="$emit('del', item.id)">删除</button>
</li>js
delTodo(id) {
if (!confirm('确定删除该任务?')) return
this.todos = this.todos.filter(item => item.id !== id)
}易错点:mouseleave 要放在整行 li 上;activeId 初值不能是已有 id;删除前确认。
底部统计、全选与清除已完成
为什么需要:底部要显示“已选中多少/全部多少”,支持全选和清除已完成。
怎么想:统计值都依赖 todos,可在 TodoFooter 中用 watch 监听 todos,并加 immediate: true 首次计算。
怎么做:
js
data() {
return { selectedCount: 0, allCount: 0, isAll: false }
},
watch: {
todos: {
immediate: true,
handler() {
this.selectedCount = 0
this.allCount = this.todos.length
this.todos.forEach(item => {
if (item.isDone) this.selectedCount++
})
this.isAll =
this.allCount > 0 && this.selectedCount === this.allCount
}
}
}全选:
js
toggleAll(checked) {
this.todos = this.todos.map(item => ({ ...item, isDone: checked }))
}清除已完成:
js
clearDone() {
this.todos = this.todos.filter(item => !item.isDone)
}易错点:watch 中累加前必须清零,否则数量越加越多;空数组时全选应为 false;清除按钮可用 v-show="selectedCount > 0"。
持久化与 axios 获取数据
为什么需要:刷新后数据会丢失,真实项目数据也通常来自接口。
怎么想:本地缓存用 localStorage;初始化 todos 时先读缓存。接口请求用 axios,在 mounted 中获取并渲染。
怎么做:
js
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
watch: {
todos: {
deep: true,
handler(val) {
localStorage.setItem('todos', JSON.stringify(val))
}
}
}js
import axios from 'axios'
mounted() {
axios.get('接口地址', { params: { q: '参数' } })
.then(res => {
this.todos = res.data
})
.catch(err => {
console.error(err)
})
}易错点:localStorage 只能存字符串,要 JSON.stringify/parse;接口参数用 params;响应体是 res.data;请求失败要处理。转写中还提到“加载中”,可在请求前设 loading=true,finally 中关闭。
从组件拆分说起:为什么数据要放在父组件
在脚手架里使用 axios 之前,先要理解数据应该放在哪。转写稿里把 App.vue 拆成了三块:头部 TodoHeader、主体 TodoMain、底部 TodoFooter。拆分时把原来 App.vue 里的结构和样式分别剪切到 src/components 下的三个组件中,公共样式抽到 src/assets/css/base.css,再在 App.vue 里通过 import 引入。
为什么要把状态定义在 App.vue?因为头部要添加任务、主体要渲染和修改选中状态、底部要统计和清除,三者是兄弟关系。如果把 todoList 定义在头部组件里,主体和底部就拿不到。所以状态必须提升到三者共同的父组件 App.vue 中,再通过 props 往下传,子组件通过自定义事件把操作“通知”回父组件。
js
// App.vue
data() {
return {
todoList: []
}
}子组件接收时用 props,转写稿里特别提到:脚手架里 props 可以写成驼峰,不必像早期写法那样强行转成短横线。
js
props: {
todoList: {
type: Array,
required: true
}
}安装并引入 axios
为什么需要 axios?因为真实项目里数据不可能写死在 App.vue 的数组中,而是来自数据库接口。脚手架中要通过 HTTP 请求拿到数据,axios 就是最常用的请求库。
怎么做?先进入项目目录安装:
bash
cnpm install axios
# 或 npm install axios然后在需要发请求的组件里引入。转写稿里强调:不引入直接用 axios 会报错。
js
import axios from 'axios'易错点:安装要在当前项目目录下执行,不是随便一个终端窗口;引入路径写 from 'axios',不要写成相对路径。
在 mounted 中发送 GET 请求
请求一般放在 mounted 里,因为此时组件已经挂载,可以安全地发起网络请求并准备渲染数据。
js
mounted() {
axios.get('接口地址', {
params: {
q: '搜索关键字',
sort: '排序字段'
}
}).then(res => {
console.log(res.data)
})
}转写稿里把查询参数放在第二个参数的 params 对象中,axios 会自动把对象转换成查询字符串拼到地址后面。调试时打开浏览器 Network 面板,能看到请求已经发出,并且有响应体。
获取响应体数据的几种写法
第一种,在 then 回调里直接取 res.data:
js
.then(res => {
console.log(res.data)
})第二种,利用解构赋值:
js
.then(({ data }) => {
console.log(data)
})第三种,用 async/await:
js
async mounted() {
const { data } = await axios.get('接口地址', {
params: { q: '...', sort: '...' }
})
console.log(data)
}转写稿里还提到,如果每次都要写 res.data 觉得麻烦,可以用拦截器统一处理响应。这是后续可以扩展的方向,本节先把请求和取数跑通。
易错点:res 是完整响应对象,里面有 status、headers、data 等,真正业务数据在 res.data 里;不要直接把 res 当成数组去渲染。
调试过程:缓存、监听与计数重置
数据请求回来后要渲染到页面,转写稿里还补了两个实战细节。
一是本地缓存。为了让刷新后数据还在,用 localStorage 存 todoList,初始化时优先读缓存:
js
data() {
const list = localStorage.getItem('todoList')
return {
todoList: list ? JSON.parse(list) : []
}
}二是统计“已选中数量”时踩的坑。转写稿里在 watch 或 updated 里遍历 todoList 累加选中项,结果数量越加越大。原因是每次执行都在旧值基础上继续 ++,没有先归零。正确做法是在遍历前先把计数重置为 0,再重新统计。
js
this.selectedCount = 0
this.todoList.forEach(item => {
if (item.isChecked) this.selectedCount++
})全选状态也类似:用“选中数量 === 全部数量”判断,但初始时两者都是 0,会误判为全选,需要额外加一个“全部数量大于 0”的条件。
易错点小结
props传数组时类型写Array,必填写required: true。- 子组件不能直接改父组件数据,要通过
$emit通知父组件修改。 axios.get的第二个参数是配置对象,查询参数放在params里。- 响应体是
res.data,不是res。 watch/updated里做累加统计前,一定先把计数变量重置。localStorage只能存字符串,存数组前要JSON.stringify,取出来要JSON.parse。
为什么要在脚手架里用 axios 获取数据
前面做的待办清单,数据是直接写在 App.vue 的 data 里,增删改查都操作这个本地数组。转写稿里提醒:真正做项目时,数据一般来自数据库或后端接口,不是自己在组件里写死一个数组。所以这一节要练习在脚手架里使用 axios 发请求、拿数据,为后面“获取数据 → 渲染数据 → 加加载中效果”做准备。
这里的“为什么需要”很明确:本地数组只能演示交互,不能代表真实项目的数据流。怎么想?先把 axios 当成一个“发请求的工具”,按“下载依赖 → 引入 → 在组件挂载后发请求 → 从响应里取数据”的顺序做。怎么做?先进入自己的项目目录,再安装 axios。转写稿里特别提到“要进入到你自己的项目当中去下载”,不要在项目外面执行安装命令。
下载并引入 axios
笔记里给出的安装命令是:
shell
cnpm install axios转写稿里说用 npm 也可以,核心是装到当前项目里。装完后,在组件里引入:
js
import axios from "axios";易错点有两个:一是忘记 import,直接写 axios.get 会报错;二是安装时没进项目目录,依赖装到了别处。转写稿里“贴你入”“A组”这些是口误/同音字,实际就是 import 和 axios。
在 mounted 中发起请求并传参数
为什么放在 mounted?因为组件挂载完成后发请求比较合适,转写稿里“毛蹄子”就是 mounted 的听写误差。怎么想?请求地址是 GitHub 的搜索仓库接口,查询参数有 q 和 sort。不要手写 ?q=r&sort=stars 拼到地址后面,而是把参数放到 axios.get 的第二个参数 params 对象里,axios 会自动拼成查询字符串。转写稿里也解释了:这个对象会被转换成查询字符串,再和地址拼接。
笔记代码如下,保留原样:
vue
<template>
<div>
<h3>App</h3>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
async mounted(){
// 1
// axios.get("https://api.github.com/search/repositories",{
// // ?q=r&sort=stars
// params:{
// q:"r",
// sort:"stars"
// }
// }).then(response=>{
// console.log(response.data);
// })
// 2
// axios.get("https://api.github.com/search/repositories",{
// // ?q=r&sort=stars
// params:{
// q:"r",
// sort:"stars"
// }
// }).then(({data})=>{
// console.log(data);
// })
// 3
const {data} = await axios.get("https://api.github.com/search/repositories",{
params: {
q: "r",
sort: "stars"
}
});
console.log(data);
}
}
</script>
<style scoped>
</style>取响应体的三种写法
转写稿里问“我想得到响应体的内容,你会怎么做”,然后逐步写出三种写法。第一种是 .then(response => console.log(response.data)):response 是整个响应对象,响应体在 response.data 里。第二种是 .then(({data}) => console.log(data)):直接在参数里解构出 data,少写一层。第三种是 async/await 加解构:const {data} = await axios.get(...),然后 console.log(data)。这三种都能拿到响应体,区别只是写法。
易错点:不要把整个 response 当成数据用,真正的数据在 response.data;用 await 时外层函数要加 async,笔记里就是 async mounted();如果只写 axios.get(...) 不 return 也不 await,后面的代码可能先执行。转写稿里还提到可以用拦截器统一处理响应,但本节先把这三种取响应体的方式掌握清楚。
调试过程与小结
调试时可以打开浏览器网络面板,刷新页面,能看到请求已经发出,并且有响应;再在控制台看 data 是否打印出来。转写稿里就是先确认“发送了一个请求”“这边有响应”,再讨论怎么取响应体。实际写的时候,先写 console.log(response) 看结构,再改成 response.data,最后再换成解构或 async/await,这样不容易把响应对象和响应体搞混。
这一节要解决什么问题
前面我们操作的是组件里写死的数组,真实项目里数据通常来自接口。本节的目标是:在 Vue 组件挂载后,用 axios 请求 GitHub 仓库搜索接口,把返回的 data.items 赋给 items,再用 v-for 渲染每个仓库的 full_name。
为什么需要:写死数据只能演示结构,不能练习“请求—取响应体—赋值—渲染”的完整链路。怎么想:先让 items 保持空数组,等组件挂载后再请求,拿到数据再更新视图。怎么做:本节笔记代码先完整保留如下。
html
<template>
<div>
<h3>App</h3>
<div v-for="item in items" :key="item.id">
<p>{{item.full_name}}</p>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
data(){
return {
items:[]
}
},
async mounted(){
const {data} = await axios.get("https://api.github.com/search/repositories",{
params: {
q: "r",
sort: "stars"
}
});
this.items = data.items;
}
}
</script>
<style scoped>
</style>易错点:items 初始值要写成 [],不要写成 null,否则模板遍历容易出问题;接口字段是 full_name,不是驼峰的 fullName。
先安装并引入 axios
为什么需要:浏览器里发请求需要请求库,转写稿里明确先下载 axios,再在组件里引入。怎么想:先确认项目目录,再安装;安装完成后在 <script> 顶部 import axios from "axios"。怎么做:在项目目录执行安装命令,例如 npm i axios 或 cnpm i axios,然后在代码里写 import axios from "axios";。易错点:不要在错误目录安装;忘记引入会报 axios is not defined;引入名要和后面使用的 axios.get 一致。
在 mounted 里发请求
为什么需要:组件要先有初始数据,等挂载完成后再发请求,避免模板一开始就依赖不存在的数据。怎么想:转写稿里说“挂载完成以后”使用 mounted;因为要 await,所以写成 async mounted()。怎么做:在 mounted 中调用 await axios.get(...),请求地址是笔记中的 GitHub 搜索地址。易错点:mounted 里用了 await,前面必须加 async;不要写成普通 mounted() 后直接 await,否则语法不通过。调试时转写稿的做法是先看 Network 里请求有没有发出,再看响应体内容。
params 与响应体 data
为什么需要:接口需要查询参数 q: "r" 和 sort: "stars"。怎么想:axios 的第二个参数可以放 params 对象,它会帮你拼成查询字符串。怎么做:按笔记写 params: { q: "r", sort: "stars" };请求结果用 const {data} = await axios.get(...) 解构出响应体,再 this.items = data.items。易错点:响应体在 data 里,不是直接 res.items;解构后变量名是 data,赋值时不要写成 this.items = items;q、sort 拼写和大小写要一致。
模板渲染与 key
为什么需要:拿到数组后要逐项显示仓库名。怎么想:v-for="item in items" 遍历,每一项用 item.full_name 输出;key 用接口提供的唯一 item.id。怎么做:模板按笔记写 :key="item.id" 和 {{item.full_name}}。易错点:key 要唯一,这里用 item.id 是合适的;字段名不要写错;如果页面空白,先确认 items.length 是否大于 0,再确认 full_name 是否存在。
调试顺序小结
转写稿的调试思路可以归纳为:先安装并引入 axios,再在 mounted 中发请求;打开网络面板确认请求和参数;查看响应体,用解构拿到 data;最后把 data.items 赋给组件状态,让模板重新渲染。遇到不显示时,按“请求是否发出 → 响应体是否有数据 → items 是否赋值 → 模板字段是否写对”的顺序排查。
课堂画面






代码示例
1. App.vue —— 通过 axios 获取数据(脚手架中)
vue
<template>
<div>
<h3>App</h3>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
async mounted(){
// 1
// axios.get("https://api.github.com/search/repositories",{
// // ?q=r&sort=stars
// params:{
// q:"r",
// sort:"stars"
// }
// }).then(response=>{
// console.log(response.data);
// })
// 2
// axios.get("https://api.github.com/search/repositories",{
// // ?q=r&sort=stars
// params:{
// q:"r",
// sort:"stars"
// }
// }).then(({data})=>{
// console.log(data);
// })
// 3
const {data} = await axios.get("https://api.github.com/search/repositories",{
params: {
q: "r",
sort: "stars"
}
});
console.log(data);
}
}
</script>
<style scoped>
</style>关键行与易错点
- 先在项目根目录安装依赖:
cnpm install axios(npm install axios亦可),否则import axios from "axios"会报模块找不到。 params对象会被 axios 自动拼成查询字符串?q=r&sort=stars,不需要自己手动拼 URL。- 写法 1 的
response.data与写法 2 的解构({data})是同一个东西:data才是响应体,response是整个响应对象。 - 写法 3 用了
await,所以mounted前面必须加async,漏写async会直接语法报错。 - 本节是“脚手架中”的用法,组件必须放在 Vue CLI 工程里运行,不能直接当普通 HTML 打开。
2. App.vue —— 渲染 axios 获取到的数据
vue
<template>
<div>
<h3>App</h3>
<div v-for="item in items" :key="item.id">
<p>{{item.full_name}}</p>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
data(){
return {
items:[]
}
},
async mounted(){
const {data} = await axios.get("https://api.github.com/search/repositories",{
params: {
q: "r",
sort: "stars"
}
});
this.items = data.items;
}
}
</script>
<style scoped>
</style>关键行与易错点
data()必须return一个对象,items初始值给空数组,模板里v-for才不会在首次渲染时报错。- 接口返回的列表在
data.items里,所以赋值是this.items = data.items,写成this.items = data会把整个响应体(含 total_count 等)当成数组用,渲染就出错。 v-for必须写:key,这里用item.id;用索引当 key 在数据增删时容易出问题。- 模板里取的是
item.full_name(GitHub 接口字段),字段名写错不会报错,只会渲染空白。 async mounted中赋值用this.items,不要写成items = ...,否则不会触发响应式更新。
常见错误
拆分组件后子组件样式全部失效
- 现象:把头部/主体/底部的 DOM 拆出去后,页面变成无样式的裸结构,控制台无报错。
- 原因:样式原封不动留在 App 的
<style scoped>里,scoped只作用于当前组件模板内的元素,不会穿透到子组件内部。 - 修法:按“谁的 DOM 谁的样式”把样式搬进对应组件;多个组件共用的按钮、li 等抽到
src/assets/base.css,在main.js或 App 中全局引入。
父组件传值给子组件,子组件拿到
undefined- 现象:
props里声明了todoList,子组件遍历时报Cannot read property 'length' of undefined。 - 原因:模板中属性名大小写被 HTML 规则吞掉(
:todoList与:todo-list不一致),或父组件根本没写绑定,只写了静态属性。 - 修法:
props声明与模板传值保持同一套命名(推荐 props 驼峰、模板短横线);给 props 写type和default: () => [],让错误在开发阶段就暴露。
- 现象:
勾选后视图不刷新,数据里其实已经改了
- 现象:点 checkbox,
console.log里isTest变了,但页面上的勾选状态和统计数字不动。 - 原因:直接修改了 props 数组里的对象属性(
item.isTest = !item.isTest),既违反单向数据流,又踩到 Vue2 对数组索引/对象属性的响应式盲区。 - 修法:
map出一份新数组,改完通过$emit('update:todoList', newList)交给父级替换;父级用新引用赋值,视图才会更新。
- 现象:点 checkbox,
“已选中数量”越点越大
- 现象:第一次勾选显示 1,再勾一个变 3,再勾变 6。
- 原因:
watch回调里用this.selectedName++累加,但每次回调前没有把计数器归零,上一轮的结果被带进了下一轮。 - 修法:回调第一行先
this.selectedName = 0再遍历;更稳妥的是改用computed,每次从todoList重新filter().length,天然没有残留。
空列表时“全选”被自动勾上
- 现象:刚进页面一条任务都没有,全选框却是选中态。
- 原因:判断条件只写了
selectedName === allName,0 === 0成立。 - 修法:补上数量大于 0 的前提,即“有任务且已选数等于总数”才算全选;
isAll用computed表达,不要在多处手动赋值。
加了 localStorage 后刷新页面数据回来了,统计数字却是 0
- 现象:列表能恢复,但“已选中/全部”显示 0,全选框状态也不对。
- 原因:只做了“写缓存”,
data初始值仍是空数组;即使读了缓存,watch也是数据变化后才执行,首次进入不会触发统计。 - 修法:
data初始化时从localStorage读取并JSON.parse,读不到再给空数组;统计逻辑加immediate: true,保证首屏算一次。
mounted里用await却拿不到数据- 现象:
const { data } = await axios.get(...)报错,或data是 undefined。 - 原因:
mounted前漏写async;或者写成了axios.get(...).then()又在外面await,两种风格混用。 - 修法:
async mounted()与await配套使用;要取响应体就解构{ data },不要直接打印整个 response。
- 现象:
全选/反选点了没反应
- 现象:点全选框,
map里也执行了,但列表勾选状态不变。 - 原因:
map之后忘了$emit把新数组交回父级,或事件名大小写与父组件监听的不一致(如updata/update、T写成大写)。 - 修法:
map完必须$emit;事件名统一小写短横线并在父子两端对照检查,命名错误是这类“逻辑没错但不生效”的高频原因。
- 现象:点全选框,
练习
组件拆分与样式归属:把现有 todoList 拆成
TodoHeader、TodoMain、TodoFooter三个组件,同时把样式按归属迁移,抽出一个base.css放公共按钮样式。完成后在笔记里列出“每条样式为什么放在这个文件”。添加任务的完整链路:在
TodoHeader中实现回车添加,要求覆盖空值提示、重复标题提示、添加成功后清空输入框三种情况。状态定义在 App,子组件只负责收集输入并$emit,说明为什么状态不能放在 Header 里。把 watch 版统计改写成 computed 版:分别实现“已选中数量、任务总数、是否全选”三个派生值,先用
watch写一版,再用computed写一版,对比两种写法在“首次进入”和“删除一条已选任务”两个场景下的表现差异。本地持久化:为 todoList 增加
localStorage缓存,要求刷新后列表、已选数量、全选状态三者一致;再手动清空缓存验证首次进入不会报错。axios 数据渲染 + 加载态:用
async/await请求 GitHub 仓库搜索接口,把结果渲染成列表,同时实现 loading 与请求失败提示。要求请求参数通过params对象传递,不要手拼查询字符串。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 对组件进行拆分 | 07:24 |
| 2 | 完成添加 | 11:39 |
| 3 | 完成修改选中的状态 | 09:16 |
| 4 | 根据ID删除任务 | 07:35 |
| 5 | 完成底部操作mp4 | 29:44 |
| 6 | 通过axios获取数据 | 09:37 |
小结
- 组件拆分的核心不是拆文件,而是先确定“状态放在共同父级、子组件只负责展示和发事件”这条边界,边界定错后面全是补丁。
- 样式要跟着 DOM 走,公共样式全局化;
scoped不是万能胶,跨组件复用样式必须显式提升到全局。 - 凡是操作数组,一律“复制—修改—回传—替换引用”,不要在原数组和 props 上就地改,这是响应式失效和单向数据流告警的共同根源。
- 派生数据(数量、全选、是否为空)优先用
computed表达,watch只留给副作用(写缓存、发请求),否则很容易写出“忘记归零”的累加 bug。 - 持久化是“读 + 写”两件事,只做一半就会出现“数据回来了、界面没回来”的假成功;初始化时机和
immediate要一起考虑。 - 异步请求要形成固定套路:
async+await+try/catch/finally,loading 和错误提示是标配而不是可选项。 - 这一节出现的多数“功能不生效”,根因是命名不一致(props、事件名、大小写)和漏写
$emit,排查时应先看父子两端的契约,再怀疑逻辑。 - 从本地数组到接口数据,变化的只是数据来源,组件结构、状态归属和更新方式应当保持不变——结构稳了,换数据源才只是换一个函数的事。