主题
商品分类案例
本页对应 1-13商品分类案例,共 2 段视频,约 21 分钟。
学习目标
- 能使用
async/await声明并调用异步函数,完成一级商品分类接口请求,并在控制台正确输出分类数组。 - 能基于一级分类数据中的
id,使用map生成由多个二级分类请求组成的 Promise 数组,且不误用await阻断映射过程。 - 能使用
Promise.all等待所有二级分类接口完成,并通过await获取全部二级分类结果。 - 能使用
map、join和innerHTML将一级标题、二级分类名称及图片渲染到页面,并正确区分外层与内层循环变量。 - 能按照“先获取一级分类、再获取所有二级分类、最后渲染数据”的步骤分阶段编码与调试,每完成一步验证一次结果。
知识点
需求与效果确认
本案例要完成“商品分类”效果:页面刷新后先没有数据,稍等片刻后同时渲染出多个区域的商品分类。每个区域都有顶部一级分类标题,下面是一排二级分类商品。

为什么需要:页面数据不是来自一个接口,而是“一级分类接口 + 多个二级分类接口”。如果拿到一个二级分类就渲染一个,页面会出现分批闪烁、顺序不稳定,也不利于统一处理。
怎么想:先把所有请求都发出去,等所有二级分类数据都回来,再一次性渲染。这正是 Promise.all 的典型场景。
怎么做:按笔记步骤:1)获取所有一级分类数据;2)遍历一级分类的 id,创建获取二级分类请求;3)合并所有二级分类 Promise 对象;4)等待同时成功,开始渲染页面。
易错点:不要误以为一级分类数据直接用于页面主体渲染;转写稿明确说页面上要渲染的二级分类,一级分类接口的数据主要用于拿 id 去请求二级分类。
先获取一级商品分类
为什么需要:二级分类接口需要一级分类 id 作为查询参数,所以必须先拿一级分类数组。
怎么想:转写稿使用 async 函数配合 await,不再写链式调用。先声明函数,再调用函数。
怎么做:关键结构如下,接口地址按接口文档替换:
js
async function getData() {
// 1. 获取一级商品分类
const res = await axios.get('一级分类接口地址')
console.log(res)
}
getData()调试时先打印 res,再点开 res.data.data,确认拿到一级分类数组,每项里有标题和 id。
易错点:声明了 async 函数却忘记调用;把 await 写错位置;没有按接口文档确认一级接口无参数。
生成二级分类的 Promise 数组
为什么需要:一级分类有几个,就要调用几次二级分类接口;而 Promise.all 管理的是 Promise 数组,所以要先基于一级分类数组生成 Promise 数组。
怎么想:基于一个数组生成新数组,用 map。每次循环里用当前项的 id 调用二级分类接口;注意这里不要 await,而是直接 return 请求返回的 Promise 对象。
怎么做:
js
const promiseArray = res.data.data.map((v) => {
const { id } = v
return axios.get('二级分类接口地址', {
params: { id }
})
})
console.log(promiseArray)转写稿中的调试过程是:打印 promiseArray 应看到 Promise 数组;打开 Network 面板,应看到一级分类请求之后发出一排二级分类请求,且每个请求的 id 不一样。
易错点:map 回调里写了 await,导致数组里变成结果而不是 Promise;忘记 return;参数名、查询参数位置没按接口文档写;变量 v 与内层变量混用。
用 Promise.all 等待全部二级分类
为什么需要:必须等所有二级分类请求都成功返回后再渲染,避免只拿到部分数据。
怎么想:Promise.all(promiseArray) 的返回值是一个新的 Promise,因此可以继续用 await 等待。
怎么做:
js
const result = await Promise.all(promiseArray)
console.log(result)调试时点开 result,它应是数组,数组每一项对应一个二级分类接口结果;再点开每项的 data.data,里面能看到标题 name、图片 picture 和子级列表 children。
易错点:忘记 await Promise.all(...);把 Promise.all 写成其他方法;没有确认每个二级接口都成功返回。
渲染:外层 map 与内层 children.map
为什么需要:页面有多个一级区域,每个区域内部又有多项二级分类,属于“数组里套数组”的渲染。
怎么想:先对外层 result 做 map 拼模板;每个区域顶部标题用 v.data.data.name。内部列表基于 children 再 map 生成 li,内层 map 后要 join(''),外层整体也要 join('')。
怎么做:关键骨架如下,模板结构按页面审查元素复制:
js
const html = result.map((v) => {
const { name, children } = v.data.data
return `
<h3>${name}</h3>
<ul>
${children.map((c) => {
return `<li>
<img src="${c.picture}" alt="">
<span>${c.name}</span>
</li>`
}).join('')}
</ul>
`
}).join('')
document.querySelector('列表容器选择器').innerHTML = html转写稿的调试方式是:先渲染结构,用“测试用”占位,确认列表出现;发现逗号后补 join('');再替换 name、picture。
易错点:内层 map 后忘记 join 出现逗号;外层忘记 join;v 和 c 用混;innerHTML 拼错;选择器没对准列表容器。
课件图示

代码示例
示例一:获取一级分类 + 生成 promise 数组 + Promise.all 拿到全部二级分类
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>01 - 获取数据</title>
</head>
<body>
<!-- 只用控制台验证数据,先不渲染 -->
<script src="https://cdn.jsdelivr.net/npm/axios@1/dist/axios.min.js"></script>
<script>
// ⚠️ 换成接口文档里的一级分类 / 二级分类地址
const API_TOP = 'http://your-host/api/category/top' // 一级分类,无参数
const API_SUB = 'http://your-host/api/category/sub' // 二级分类,需要 id(query 查询参数)
// 1. 声明 async 函数
async function getCategoryData() {
// 2. 获取一级商品分类
const res = await axios({ url: API_TOP })
console.log('一级分类:', res.data.data) // 数组,每项里有 id 和 name
// 3. 基于每个 id 生成一个 promise,组成 promise 数组
const pArray = res.data.data.map((v) => {
const { id } = v
// 注意:这里直接 return,不要写 await
return axios({ url: API_SUB, params: { id } })
})
console.log('promise 数组:', pArray)
// 4. Promise.all 等待所有二级分类同时成功
const result = await Promise.all(pArray)
console.log('所有二级分类:', result)
return result
}
// 5. 声明完一定要调用
getCategoryData()
</script>
</body>
</html>要点 / 易错点
async只是修饰符,函数声明完必须手动调用getCategoryData(),否则一行都不会执行。- 一级分类接口返回的是数组,
res.data.data才是真正的分类数组(axios 外面包了一层)。 map的回调里直接return axios(...):axios 调用的返回值本身就是 promise 对象,所以pArray才是「promise 数组」,才能交给Promise.all。- 二级分类接口的参数是 query 查询参数,所以要写在
params: { id }里,不是拼在 url 上。 - 二级分类接口要调「一级分类的个数」那么多次,网络面板里能看到一串请求,每个 id 都不一样——这就是要
Promise.all的原因。
示例二:渲染数据(map + join + innerHTML,内外双层循环)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>02 - 渲染数据</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.sub-list { display: flex; flex-wrap: wrap; gap: 24px; padding: 20px; }
.sub-list > li { list-style: none; width: 300px; }
.sub-list h3 { font-size: 16px; margin-bottom: 10px; }
.sub-list ul { display: flex; flex-wrap: wrap; gap: 10px; }
.sub-list ul li { list-style: none; width: 84px; text-align: center; font-size: 12px; }
.sub-list img { display: block; width: 60px; height: 60px; margin: 0 auto 4px; }
</style>
</head>
<body>
<!-- 大容器里只放一个列表盒子,其余全靠 JS 生成 -->
<div class="sub-list"></div>
<script>
// 模拟 Promise.all 之后拿到的 result,结构与真实接口完全一致
const result = [
{
data: {
data: {
name: '居家',
children: [
{ name: '巧克力', picture: 'https://picsum.photos/seed/a/60/60' },
{ name: '拖鞋', picture: 'https://picsum.photos/seed/b/60/60' }
]
}
}
},
{
data: {
data: {
name: '服饰',
children: [
{ name: '衬衫', picture: 'https://picsum.photos/seed/c/60/60' },
{ name: '裤子', picture: 'https://picsum.photos/seed/d/60/60' }
]
}
}
}
]
// 外层:一个一级分类 -> 一个 li
const html = result.map((v) => {
// 解构出顶级标题 name 和二级分类数组 children
const { name, children } = v.data.data
return `<li>
<h3>${name}</h3>
<ul>
${children.map((c) => `
<li>
<img src="${c.picture}" alt="" />
<span>${c.name}</span>
</li>`).join('')}
</ul>
</li>`
}).join('') // 别忘了外层也要 join
// 一次性渲染
document.querySelector('.sub-list').innerHTML = html
</script>
</body>
</html>要点 / 易错点
- 数据是「数组套数组」:外层
map生成一级分类的li,内层children.map生成二级分类的li。 - 每个
map后面都必须.join(''),否则数组被隐式转成字符串,页面上会多出一堆逗号(视频里那个「逗号从哪来的」就是这个坑)。 - 内层循环的变量用
c,外层用v,两个变量名千万别写混,否则取不到name/picture。 - 内层
map的结果直接嵌在外层模板字符串里,所以内层不需要再写innerHTML;整个页面只在最后赋值一次。 - 二级分类的字段是
name和picture,图片要放进img的src,文本放进span。 - 建议写一步测一步:先
console.log(name)确认标题通了,再替换图片。
示例三:完整整合版(获取 + 等待 + 渲染,带异常兜底)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>03 - 商品分类完整版</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.sub-list { display: flex; flex-wrap: wrap; gap: 24px; padding: 20px; }
.sub-list > li { list-style: none; width: 300px; }
.sub-list h3 { font-size: 16px; margin-bottom: 10px; }
.sub-list ul { display: flex; flex-wrap: wrap; gap: 10px; }
.sub-list ul li { list-style: none; width: 84px; text-align: center; font-size: 12px; }
.sub-list img { display: block; width: 60px; height: 60px; margin: 0 auto 4px; }
</style>
</head>
<body>
<div class="sub-list"></div>
<script src="https://cdn.jsdelivr.net/npm/axios@1/dist/axios.min.js"></script>
<script>
// ⚠️ 换成接口文档里的真实地址
const API_TOP = 'http://your-host/api/category/top'
const API_SUB = 'http://your-host/api/category/sub'
async function init() {
try {
// 第一步:获取一级商品分类
const res = await axios({ url: API_TOP })
// 第二步:获取所有的二级商品分类
const pArray = res.data.data.map((v) => {
const { id } = v
return axios({ url: API_SUB, params: { id } }) // 直接 return,不写 await
})
const result = await Promise.all(pArray) // 全部成功后再往下走
// 第三步:渲染数据
const html = result.map((v) => {
const { name, children } = v.data.data
return `<li>
<h3>${name}</h3>
<ul>
${children.map((c) => `
<li>
<img src="${c.picture}" alt="" />
<span>${c.name}</span>
</li>`).join('')}
</ul>
</li>`
}).join('')
document.querySelector('.sub-list').innerHTML = html
} catch (err) {
console.log('数据获取失败:', err)
}
}
init()
</script>
</body>
</html>要点 / 易错点
- 三步顺序不能乱:先拿一级分类 → 才能基于 id 生成 promise 数组 →
Promise.all等待全部成功 → 最后才渲染。 Promise.all的返回值是一个新的 promise,所以可以直接await Promise.all(pArray),不需要再写.then。- 只要有一个二级分类请求失败,
Promise.all就会直接进入catch,页面不会渲染——这正是「同时渲染」的语义。 - 只要「多个接口都成功才渲染」这种场景,第一反应就是
Promise.all。 - 渲染只在最后做一次
innerHTML,中途不要反复赋值,避免页面闪烁和性能浪费。
常见错误
现象:Network 面板里二级分类请求依次排队发出,总耗时等于各请求耗时相加,页面加载明显变慢。原因:在
map回调里写了async v => await axios(...),虽然返回值仍是 Promise、Promise.all也能用,但循环体内部被await阻塞,请求变成串行。修法:回调里直接return axios(...),把“等待”只留给最外层的await Promise.all(...)。现象:
Promise.all拿到的是[undefined, undefined, ...],渲染时报Cannot read properties of undefined。原因:map回调用了带花括号的函数体却漏写return(或者误用了forEach,它本身不返回数组)。修法:补上return,或改成arr.map(v => axios(...))的简写形式;先console.log(pArr)确认数组里装的是 Promise 对象。现象:页面完全没反应,Network 里一条请求都没有。原因:只声明了
async function getData(){},忘记在末尾调用。修法:声明后立即调用一次;建议在函数体第一行打个 log,验证执行链路。现象:标题、图片位置显示
undefined,或者列表区域空白。原因:解构层级或字段名写错(data/data.data混用、children、picture、name拼错或大小写不一致)。修法:每写完一层解构就先console.log验证一次,再往下写;渲染前先打印一条完整数据结构作为对照。现象:列表项之间多出一个逗号。原因:内层
children.map(...)得到的是数组,直接塞进模板字符串时会走数组的隐式toString,用逗号连接。修法:在map后补.join(''),再替换进模板。现象:内层列表渲染出的是外层的数据,或所有区域内容一模一样。原因:内外层
map用了同名变量(都写v),内层回调拿到的是外层项,或遮蔽后取值错误。修法:外层用v、内层用c(或语义化命名category/sub),写完逐行核对变量来源。现象:
document.querySelector(...)返回null,报Cannot set properties of null。原因:选择器类名拼写与 HTML 不一致(漏点、大小写、层级写错)。修法:在浏览器里复制该元素的 selector 再粘贴,赋值前先打印一次查询结果。
练习
并行 vs 串行对比:把当前
map + Promise.all的实现,另存一份改为for...of+await逐个请求二级分类。清空缓存后各跑一次,用 Network 面板记录两种方式的总耗时与请求瀑布图,写三行结论说明何时该选哪种。抽离渲染函数:把渲染逻辑抽成
render(list),要求:(a) 主流程只剩“取数 →Promise.all→render”三步;(b) 数据为空数组时,页面显示“暂无分类”占位而不是空白;(c) 不改动原有 DOM 结构。容错处理:人为把某一个二级分类的 id 改成不存在的值,让其中一条请求失败。要求页面仍然渲染其余成功的分类,失败的那一项显示兜底文案(提示:
Promise.allSettled或给每个请求加.catch,二选一,并说明你为什么选它)。换一种 DOM 写法:把
map + join + innerHTML改写成document.createElement+appendChild(或insertAdjacentHTML)的方式,两种实现都跑通后,从性能、可读性、安全性(XSS)三个角度各写一句对比。懒加载改造:把“一次性并发请求所有二级分类”改成“点击一级标题时才请求并渲染该分类下的二级分类”,要求同一分类重复点击不重复发请求。完成后说明:这个场景下为什么原来的
Promise.all方案不再是最优解。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 案例-商品分类01 | 11:37 |
| 2 | 案例-商品分类02 | 09:37 |
小结
这个案例的骨架是“一个前置请求 → 由前置结果派生 N 个请求 → 等全部回来再统一渲染”,
Promise.all只负责最后那一步“等全部”。map在这里的唯一职责是把 id 数组转换成 Promise 数组,回调里绝不能加await,一加就从并行退化为串行。Promise.all的入参必须是 Promise 数组:forEach返回undefined、漏写return返回undefined元素,都会让它在真正开始执行前就失败。await可以等待任何 Promise,包括Promise.all的返回值,所以不需要写.then链,用async/await读起来更线性。两层嵌套渲染时,外层
map的返回值里可以再套一层map(...).join(''),但每个map都必须配一个join(''),否则数组会隐式转成逗号串。变量命名(
v/c)和解构层级是这类嵌套渲染最容易出错的地方,写一步console.log验证一步,比写完再调试快得多。Promise.all是“一失败全失败”,真实业务里若需要部分成功,应换Promise.allSettled或给单个请求加catch兜底。当请求之间没有依赖、只是都要用结果时用并发;当后一个请求依赖前一个结果、或需要按需触发时,串行/懒加载才是正确选择。