Skip to content

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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

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 的两个回调只会执行一个。
  • 四种情况归纳:
    1. 不写 return:p2 成功,PromiseResult 为 undefined;
    2. return 非 promise 数据:p2 成功,PromiseResult 为该数据;
    3. return 一个 promise:p2 与它的状态、结果完全一致;
    4. 回调里出现运行错误: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。

常见错误 ​

  1. 封装成 Promise 版后仍传 success/error 回调 → 原因:函数已改为返回 promise,回调应通过 then/catch 指定 → 修法:去掉调用处的 success/error,在封装内部 resolve/reject,调用处用 .then(...).catch(...)。
  2. 链式调用中忘记 return ajaxPromise(...) → 原因:then 回调的返回值决定下一个 promise 的状态和结果,不 return 会得到 undefined 并让后续逻辑错位 → 修法:需要串联的异步请求必须 return 下一段 promise。
  3. 失败分支只 console.log,不中断链条 → 原因:失败回调没有抛错或返回 rejected promise,后续 then 仍可能继续执行 → 修法:失败分支 throw err 或 return Promise.reject(err),末尾统一 catch。
  4. 在 then 外同步读取请求结果 → 原因:then 回调异步执行,promise 结果不会立刻可用 → 修法:所有依赖结果的代码放进 then/catch 或改用 async/await。
  5. 以为 then 的两个回调都会执行 → 原因:成功/失败回调互斥,只会执行其中一个 → 修法:明确成功与失败分支,不要把清理逻辑只写在某一侧。
  6. 封装时 resolve/reject 传错值或忘记传响应体 → 原因:resolve 的参数就是后续 then 拿到的 PromiseResult → 修法:成功传 xhr.response,失败传结构化错误信息,并保证分支后 return。

练习 ​

  1. 将给定 ajax 回调版函数改造成 Promise 版:调用时只传 URL、method、data、dataType,不传 success/error;HTTP 200 时 resolve(response),非 200 时 reject({code, msg}),网络错误时 reject(error)。
  2. 用 Promise 链式调用完成三步请求:获取榜单列表 → 取第一个榜单 ID 获取歌曲列表 → 取第一首歌 ID 获取歌曲详情;要求每步都 return 下一请求,并统一处理错误。
  3. 用代码验证 then 回调四种情况对后续 then 的影响:无返回值、返回非 promise 值、返回 promise、抛出运行错误;分别打印后续状态和结果。
  4. 改造练习 2:要求第一步失败时后续请求不再执行,控制台只输出一次可读错误;说明应使用 throw、Promise.reject 还是 catch。
  5. 用 async/await + try/catch 重写练习 2,并对比链式写法中 return 的位置与错误捕获范围。

原视频对照 ​

#视频时长
1封装ajax promise 函数26:11
2promise对象的then方法25:58
3ajax的回调地狱24:45
4then链式调用解决回调地狱12:13
5关于链式调用中出现错误04:51

小结 ​

  1. Promise 封装 ajax 的核心不是“换语法”,而是“返回 promise,把回调外置到 then/catch”。
  2. then 会返回新 promise,后续状态由被执行回调的返回值决定;串联异步任务时必须 return。
  3. 成功回调和失败回调互斥,不要按“两个都会走”的假设写逻辑。
  4. 失败分支不抛错、不返回 rejected promise,会让链条继续往下走,这是链式调用最常见的隐性 bug。
  5. 链式调用解决回调地狱的关键是“每步返回下一异步任务”,并用末尾 catch 统一兜底。
  6. 请求结果只能在 then/catch/await 之后使用,不能在同步代码里直接读取。
  7. resolve/reject 只生效一次,分支处理后应及时 return,避免继续执行。
  8. 实际项目可优先使用 async/await,但必须理解 then 链的返回值和错误传播规则。