主题
Promise 概述与Promise 基本语法(Day16 2/3)
本页对应 Day16_Promise,共 5 段视频,约 1 小时 34 分。
学习目标
- 能独立将基于回调的 ajax 函数改造成返回 Promise 对象的
ajaxPromise函数,并在请求成功时resolve响应体、失败时reject错误信息。 - 能准确说明
then方法两个回调函数的执行条件,并判断回调无返回值、返回非 Promise 值、返回 Promise 对象、抛出运行错误四种情况下then返回 Promise 的状态与结果。 - 能识别多层 ajax 嵌套形成的回调地狱,说明其可读性问题和按依赖顺序嵌套的原因。
- 能使用
then链式调用改写三层依赖的 ajax 请求,并按“获取榜单列表 → 获取榜单歌曲 → 获取歌曲详情”的顺序取得目标数据。 - 能分析链式调用中某一步请求失败或回调内发生错误时的传播结果,解释后续回调为何执行失败分支以及中间结果为何被跳过。
知识点
Promise 是什么,为什么需要它
为什么需要:传统异步编程常把“成功回调”“失败回调”直接塞进调用参数里,一旦异步操作有先后依赖,就会写成一层套一层的回调,转写稿里称为“回调地狱”。Promise 是 ES6 新增的异步编程解决方案,用来更友好地表示一个异步操作的最终结果。
怎么想:把 Promise 看成一个“状态机”。通过 new Promise 创建对象,内部放异步操作;异步操作成功后调用 resolve,失败后调用 reject。它有三种状态:pending(进行中,刚创建时)、resolved/fulfilled(已成功)、rejected(已失败)。状态只能从 pending 变成成功或失败,且“当 promise 的状态发生改变,就再也不会变了”。
怎么做:基本形式如下:
js
const p = new Promise((resolve, reject) => {
// 异步操作
// 成功:resolve(result)
// 失败:reject(error)
});
p.then(
result => { /* 成功处理 */ },
error => { /* 失败处理 */ }
);易错点:resolve 和 reject 只会有一个真正生效;状态改变后不能回退;Promise 执行器函数是同步执行的,而 then 里的回调是异步执行的,所以不要刚创建完就立刻打印新 Promise 的状态来判断结果。
从 Ajax 封装看 Promise 的基本用法
为什么需要:原版 Ajax 封装通常要求调用时传入 success 和 error 回调,调用与结果处理绑在一起。转写稿中把它改造成 Promise 版,就是为了让函数“先返回 Promise 对象”,调用者再用 then 处理结果。
怎么想:ajaxPromise 内部仍然创建 XMLHttpRequest,设置 url、请求方式、responseType = 'json',在 onreadystatechange 中判断响应是否完成、状态码是否成功。成功就 resolve(xhr.response),失败就 reject(...)。外部不再传回调,而是通过返回的 Promise 接收结果。
怎么做:核心思路是:
js
function ajaxPromise(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response);
} else {
reject({ code: 1001, msg: '未能获取到正确的内容' });
}
}
};
xhr.send();
});
}易错点:Ajax 请求涉及协议、域名、端口,转写稿中特意用 Live Server 打开页面,而不是直接双击 HTML;否则某些接口会因缺少协议域名端口而失败。另外,resolve 传的是响应体,后续 then 拿到的就是它。
then 方法的参数与返回值
为什么需要:then 不仅用于指定成功/失败回调,它的返回值还是新 Promise,这是链式调用的基础。
怎么想:then 返回的肯定是 Promise 对象,但这个新 Promise 的状态不由 then 自己决定,而由“被执行的那个回调函数的返回值”决定。两个回调只会执行一个:前一个 Promise 成功执行第一个,失败执行第二个。
怎么做:转写稿总结了四种情况:1)回调没有返回值,新 Promise 为成功,result 是 undefined;2)回调返回非 Promise 数据,新 Promise 为成功,result 是该返回值;3)回调返回 Promise,新 Promise 的状态和结果与返回的 Promise 保持一致;4)回调中出现运行错误,新 Promise 变为失败,错误对象作为结果。
易错点:不要以为前一个 Promise 失败,then 返回的新 Promise 就一定失败。转写稿的错误链分析中,第一个请求失败后,失败回调只 console.log 没有 return,于是 then 返回的新 Promise 仍是成功,后续代码继续执行,读取 undefined 才报错,最终又触发后面的失败。这就是“失败回调不返回内容”的隐蔽坑。
用 then 链式调用解决回调地狱
为什么需要:转写稿演示了“获取榜单列表 → 根据榜单 ID 获取歌曲列表 → 根据歌曲 ID 获取歌曲详情”的三层依赖。传统写法必须把下一步请求写在上一步回调里,层层嵌套。
怎么想:每一步都 return 下一个 Ajax Promise,让 then 返回的新 Promise 跟随这个请求;这样代码从“嵌套”变成“顺着写”,看起来像同步,实际仍是异步按序执行。
怎么做:结构可以简化为:
js
ajaxPromise(topListUrl)
.then(res => {
const id = res.data.list[0].id;
return ajaxPromise(playlistUrl + id);
}, err => { console.log('获取榜单失败'); })
.then(res => {
const sid = res.data.songs[0].id;
return ajaxPromise(songDetailUrl + sid);
}, err => { console.log('获取歌曲列表失败'); })
.then(res => {
console.log(res);
}, err => { console.log('获取歌曲信息失败'); });易错点:链式调用中必须 return 下一个 Promise,否则下一步拿不到正确结果;失败回调里也要考虑是否继续向下传递,否则会出现“前面失败、后面还执行”的调试现象。转写稿最后强调,Promise 链式调用不是消除异步,而是把异步依赖写得更清晰,是必须掌握的写法。
一、创建 Promise 对象:构造函数与执行器
为什么需要:回调写法里,请求成功、失败的处理函数要在调用时一并传进去,调用和处理混在一起。Promise 先把“异步任务”包装成一个对象,后面再用 then 指定成功、失败怎么处理,写法上把调用和处理分开了。
怎么想:new Promise(...) 是实例化 Promise 类,必须传一个回调函数作为参数。这个回调函数常叫执行器(executor)。转写稿里封装 ajaxPromise 时,就是把创建 XHR、open、send 这些动作放进 new Promise 的回调里。关键要记住:这个回调在实例化 Promise 时会自动调用,而且是同步任务,不是等到 then 才执行。
怎么做:
js
new Promise((res, rej) => {
console.log('第一个参数:', res);
console.log('第二个参数:', rej);
});js
1. 实例化 Promise 类,需要传一个回调函数作为参数
2. Promise 类的回调函数(参数),在实例化 Promise 的时候会自动调用,是同步任务
3. Promise 类的回调函数(参数),在被调用的时候,会接收两个参数,两个参数都是函数易错点:不要以为 new Promise 只是“声明一下”,里面的代码会立刻同步执行;也不要把执行器的两个参数当成普通值,它们都是函数。转写稿里强调“这个回调函数是同步执行还是异步执行?它是同步执行的”,所以创建 XHR 等动作可以放进去。
二、改变状态:resolve、reject 与异常
为什么需要:Promise 对象要有状态,then 才知道该执行成功回调还是失败回调。状态改变时还要能把结果带出去,这个结果就是 PromiseResult。
怎么想:执行器收到两个函数:第一个调用后把状态改为 resolved(fulfilled),第二个调用后改为 rejected。转写稿封装 ajaxPromise 时,请求成功、拿到响应结果后调用 resolve(xhr.response);请求失败或没拿到正确内容时调用 reject(...)。另外,执行器里抛异常,也会让 Promise 变成 rejected,值是异常信息。
怎么做:
js
new Promise((resolve, reject) => {
// 调用第一个参数 该promise对象的状态改为 resolved(fulfilled)
// 可以传个参数作为 PromiseResult
// resolve('hello');
// resolve({status:'OK', msg: 'success'});
// 调用第二个参数, 该promise对象的状态改为 rejected
// 可以传个参数作为 PromiseResult
// reject();
reject([10,20,30,40]);
});js
1. 调用 Promise 类回调函数的第一个参数,该 promise 对象改为 resolve(fulfilled)状态,可以传个参数作为 PromiseResult
2. 调用 Promise 类回调函数的第二个参数,该 promise 对象改为 rejected 状态,可以传个参数作为 PromiseResult
3.promise 类回调函数中抛出异常promise对象改为rejected状态,值是异常信息
3. promise 对象的状态一旦改变,就无法再修改易错点:状态一旦从 pending 改成成功或失败,就不能再改;不要既 resolve 又 reject 指望后者生效。reject 的参数可以是任意值,不一定是 Error。执行器里如果一直不调用 resolve/reject,Promise 会停在 pending。
三、用 then 设置回调:成功与失败分支
为什么需要:Promise 对象本身不直接写业务处理,而是通过 then 注册回调:成功走第一个回调,失败走第二个回调,并通过形参拿到 PromiseResult。
怎么想:then 的第一个参数是成功回调,第二个参数是失败回调。转写稿里反复提醒:then 的两个回调都是异步执行,即使 Promise 已经是成功状态,回调也要等同步代码走完。调试时不要一创建 Promise 就立刻打印对象状态,因为状态可能还没在回调里体现;更稳的是在 then 的回调里看结果。
怎么做:
js
promise对象.then(res => {
// 如果promise对象是成功状态,执行该回调函数
// res 可以获取 PromiseResult
}, err => {
// 如果promise对象是失败状态,执行该回调函数
// res 可以获取 PromiseResult
});js
1. promise对象的then方法第第一个参数(回调函数),状态变为成功会执行,可以通过形参得到 PromiseResult
2. promise对象的then方法第第二个参数(回调函数),状态变为失败会执行,可以通过形参得到 PromiseResult
3. then 方法的两个回调函数都是异步执行!易错点:then 的两个回调只会执行一个;成功回调的形参拿的是 resolve 的值,失败回调拿的是 reject 的值。转写稿里调试“地址不对”时,请求失败会走失败回调,说明失败分支不是靠猜,而是由状态决定。还要注意:then 回调异步执行,不要把它当同步代码顺序理解。
课堂画面





代码示例
1. Promise 基本语法:创建对象、修改状态、设置回调
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Promise 基本语法</title>
</head>
<body>
<script>
// ① 实例化 Promise 类,参数是一个回调函数
// 这个回调函数在 new 的时候会【自动调用】,属于同步任务
// 调用时接收两个参数,两个参数都是函数
const p = new Promise((resolve, reject) => {
console.log('第一个参数:', resolve);
console.log('第二个参数:', reject);
// ② 修改状态
// 调用第一个参数 -> pending 变为 resolved(fulfilled),可传参作为 PromiseResult
resolve('hello');
// resolve({ status: 'OK', msg: 'success' });
// 调用第二个参数 -> pending 变为 rejected,可传参作为 PromiseResult
// reject([10, 20, 30, 40]);
// 状态一旦改变,就再也不会变了,所以下面这句无效
reject('改不动了');
});
// ③ 为 promise 对象设置回调
p.then(res => {
// 成功状态执行,形参能拿到 PromiseResult
console.log('成功,PromiseResult =', res); // hello
}, err => {
// 失败状态执行,形参能拿到 PromiseResult
console.log('失败,PromiseResult =', err);
});
console.log('我先执行,因为 then 的两个回调都是异步的');
</script>
</body>
</html>关键行与易错点
new Promise((resolve, reject) => {...}):传入的回调是同步执行的,所以两行console.log会最先打印,早于then里的回调,也早于最后一行console.log。resolve('hello'):状态由pending -> resolved,参数'hello'成为PromiseResult,被then第一个回调的形参接收。reject(...):状态由pending -> rejected,被then第二个回调接收。- 易错点 1:
resolve之后再写reject是无效的——“状态一旦改变,就再也不会变了”。 - 易错点 2:
then的两个回调是异步执行的,不要以为then写在前面就先执行;实际输出顺序是:两个console.log(参数)→ 最后一行 →then成功回调。 - 易错点 3:
then的两个回调只会执行一个,取决于 promise 对象的最终状态。
2. then 方法的返回值:四种情况
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>then 的返回值</title>
</head>
<body>
<script>
// 第一步:先创建一个 promise 对象 p1
const p1 = new Promise((resolve, reject) => {
resolve('success'); // 设置成功状态
// reject('error'); // 换成这句,p1 就是失败状态
});
// 第二步:p1 调用 then,返回值赋给 p2
// p2 百分之百是一个 promise 对象
const p2 = p1.then(res => {
// 情况一:没有返回值(不写 return)
// -> p2 是【成功】状态,PromiseResult 是 undefined
// return undefined;
// 情况二:返回非 promise 类型的数据
// -> p2 是【成功】状态,PromiseResult 就是返回的这个值
// return 100;
// 情况三:返回一个 promise 对象
// -> p2 的状态和 PromiseResult 与它【完全保持一致】
// return new Promise((res, rej) => res('成功'));
// 情况四:回调中出现运行错误(比如调用了不存在的函数)
// -> 错误不会向外抛出,而是把 p2 改成【失败】状态,错误对象作为 PromiseResult
// ajaxPromise(); // 未定义的函数
}, err => {
console.log('p1 失败,执行的是这个回调', err);
});
// 第三步:给 p2 设置回调,观察 p2 的状态和结果
p2.then(res => {
console.log('p2 成功,PromiseResult =', res);
}, err => {
console.log('p2 失败,PromiseResult =', err);
});
</script>
</body>
</html>关键行与易错点
const p2 = p1.then(...):then的返回值一定是 promise 对象,这一点毫无疑问;不确定的只是它的状态。- 状态由谁决定?——由被执行的那个回调函数的返回值决定。p1 成功就执行第一个回调,p1 失败就执行第二个回调,
then的两个回调只会执行一个。 - 四种情况归纳:
- 不写
return:p2 成功,PromiseResult为undefined; return非 promise 数据:p2 成功,PromiseResult为该数据;return一个 promise:p2 与它的状态、结果完全一致;- 回调里出现运行错误:p2 失败,错误对象作为
PromiseResult(类似try...catch,不会把错误抛出去)。
- 不写
- 易错点 1:不要一创建
p2就立刻console.log(p2)看状态——then的回调是异步的,此时状态还没变,要用回调观察。 - 易错点 2:情况四里错误不会中断整个程序,而是“变成”失败状态的 promise,所以能一路往下走到失败回调。
- 易错点 3:p1 是失败状态跟 p2 是什么状态没有关系,p2 只看“被执行的那个回调返回了什么”。
3. 封装 ajax 为 promise + then 链式调用解决回调地狱
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ajax 的 promise 封装与链式调用</title>
</head>
<body>
<h1>排行榜第一首歌</h1>
<div id="box"></div>
<script>
// ============ 一、封装:返回 promise 对象,不再接收成功/失败回调 ============
function ajaxPromise(options) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', options.url);
xhr.responseType = options.dataType || 'json'; // 接口返回 JSON 就设 json
xhr.onload = function () {
if (xhr.status === 200) {
// 拿到响应结果后改变状态:成功,响应体作为 PromiseResult
resolve(xhr.response);
} else {
// 响应回来了但没拿到正确内容
reject({ code: 1001, msg: '未能获取到正确的内容' });
}
};
xhr.onerror = function () {
// 请求压根没发出去
reject({ code: 101, msg: '未能成功发送请求' });
};
xhr.send(options.body || null);
});
}
// 接口地址:按后端文档填写(文档上看到的只是路径,前面要拼上域名和端口)
const BASE_URL = 'http://localhost:3000';
const box = document.getElementById('box');
// ============ 二、链式调用:顺着写,不再一层套一层 ============
// 第一步:获取所有榜单
ajaxPromise({ url: BASE_URL + '/toplist', dataType: 'json' })
.then(res => {
// 取出第一个榜单的 ID —— 只有这里才能拿到 ID
const id = res.data.list[0].id;
console.log('榜单 ID:', id);
// 返回一个 promise,后面的 then 就会等它
return ajaxPromise({ url: BASE_URL + '/playlist/detail?id=' + id, dataType: 'json' });
}, err => {
console.log('获取榜单列表失败');
})
// 第二步:根据榜单 ID 获取该榜单下的歌曲列表
.then(res => {
const sid = res.playlist.trackIds[0].id; // 第一首歌的 ID
console.log('歌曲 ID:', sid);
return ajaxPromise({ url: BASE_URL + '/song/detail?ids=' + sid, dataType: 'json' });
}, err => {
console.log('获取歌曲列表失败');
})
// 第三步:根据歌曲 ID 获取歌曲详细信息
.then(res => {
const song = res.songs[0];
box.innerHTML =
`歌名:${song.name}<br>` +
`歌手:${song.ar.map(item => item.name).join(' / ')}<br>` +
`专辑:${song.al.name}`;
}, err => {
console.log('获取歌曲信息失败');
});
</script>
</body>
</html>关键行与易错点
return new Promise((resolve, reject) => {...}):改造的核心。原来靠success/error两个回调通知结果,现在把它们换成resolve/reject,函数直接返回 promise 对象,调用处就不再传回调了。resolve(xhr.response):一定要放在拿到响应结果之后(onload里status === 200时),否则 promise 会立刻变成成功状态,PromiseResult是空的。return ajaxPromise({...}):这一步是链式调用的关键。回调里return一个 promise,后一个then就在等它——因为“返回 promise 时,then 的返回值与它状态、结果完全一致”,相当于把三个请求串成了一条链。- 为什么必须在上一个
then里发下一个请求?因为上一步的结果是下一步的参数:不先拿到榜单 ID,就没法拼?id=;写在链外面的话,异步请求还没回来,ID 根本不存在。 - 易错点 1:失败回调里如果没有
return,这个then返回的仍然是成功状态的 promise,链条会继续往下走,后面的成功回调会拿到undefined,于是res.data直接报错,最后一路错到底——所以失败回调里要么return一个失败的 promise,要么throw。 - 易错点 2:拼接口地址时不要漏掉前面的域名和端口,也不能直接双击 HTML 打开(没有协议、域名、端口,接口可能取不到),要用 Live Server 之类的静态服务器打开。
- 易错点 3:
dataType忘了设成json,拿到的是字符串,res.data取不到东西。 - 易错点 4:链式调用里写箭头函数用大括号时,别忘了
return——少了return,链条上就断了一环,后面的回调拿到的是undefined。
常见错误
- 封装成 Promise 版后仍传
success/error回调 → 原因:函数已改为返回 promise,回调应通过then/catch指定 → 修法:去掉调用处的success/error,在封装内部resolve/reject,调用处用.then(...).catch(...)。 - 链式调用中忘记
return ajaxPromise(...)→ 原因:then回调的返回值决定下一个 promise 的状态和结果,不 return 会得到undefined并让后续逻辑错位 → 修法:需要串联的异步请求必须return下一段 promise。 - 失败分支只
console.log,不中断链条 → 原因:失败回调没有抛错或返回 rejected promise,后续then仍可能继续执行 → 修法:失败分支throw err或return Promise.reject(err),末尾统一catch。 - 在
then外同步读取请求结果 → 原因:then回调异步执行,promise 结果不会立刻可用 → 修法:所有依赖结果的代码放进then/catch或改用async/await。 - 以为
then的两个回调都会执行 → 原因:成功/失败回调互斥,只会执行其中一个 → 修法:明确成功与失败分支,不要把清理逻辑只写在某一侧。 - 封装时
resolve/reject传错值或忘记传响应体 → 原因:resolve的参数就是后续then拿到的PromiseResult→ 修法:成功传xhr.response,失败传结构化错误信息,并保证分支后return。
练习
- 将给定 ajax 回调版函数改造成 Promise 版:调用时只传 URL、method、data、dataType,不传
success/error;HTTP 200 时resolve(response),非 200 时reject({code, msg}),网络错误时reject(error)。 - 用 Promise 链式调用完成三步请求:获取榜单列表 → 取第一个榜单 ID 获取歌曲列表 → 取第一首歌 ID 获取歌曲详情;要求每步都
return下一请求,并统一处理错误。 - 用代码验证
then回调四种情况对后续then的影响:无返回值、返回非 promise 值、返回 promise、抛出运行错误;分别打印后续状态和结果。 - 改造练习 2:要求第一步失败时后续请求不再执行,控制台只输出一次可读错误;说明应使用
throw、Promise.reject还是catch。 - 用
async/await + try/catch重写练习 2,并对比链式写法中return的位置与错误捕获范围。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 封装ajax promise 函数 | 26:11 |
| 2 | promise对象的then方法 | 25:58 |
| 3 | ajax的回调地狱 | 24:45 |
| 4 | then链式调用解决回调地狱 | 12:13 |
| 5 | 关于链式调用中出现错误 | 04:51 |
小结
- Promise 封装 ajax 的核心不是“换语法”,而是“返回 promise,把回调外置到
then/catch”。 then会返回新 promise,后续状态由被执行回调的返回值决定;串联异步任务时必须return。- 成功回调和失败回调互斥,不要按“两个都会走”的假设写逻辑。
- 失败分支不抛错、不返回 rejected promise,会让链条继续往下走,这是链式调用最常见的隐性 bug。
- 链式调用解决回调地狱的关键是“每步返回下一异步任务”,并用末尾
catch统一兜底。 - 请求结果只能在
then/catch/await之后使用,不能在同步代码里直接读取。 resolve/reject只生效一次,分支处理后应及时return,避免继续执行。- 实际项目可优先使用
async/await,但必须理解then链的返回值和错误传播规则。