Promise-async/await

基于回调的异步编程

在需要远程请求脚本文件,并执行其中函数时,可以使用异步方式。
setTimeout 函数允许异步执行。

function loadScript(src, callback) {
  let script = {src: '', onload: () => {}, onerror: () => {}};
  script.src = src;
  console.info("开始模拟网络请求...");
  // 模拟网络请求延时
  script.onload = () => setTimeout(callback, 1000, script);
  // 模拟完成后触发
  script.onload();
  console.info("同步代码块执行完成...");
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js ', script => {
  console.info(script.src + " 异步执行加载完成");
});

Promise

生产消费模型

  • 生产者:执行一些任务,并且有一定的耗时(通过网络加载脚本)
  • 消费者:在生产者完成生产后立即获取生产结果(需要用到脚本内函数的方法)
  • Promise:连接生产者与消费者的特殊对象,类似于订阅列表,当生产者完成生产时,向所有订阅了的代码开放结果

Promise 本质上会更加复杂,并不是简单的生产消费模型。

生产者 executor

语法签名:

let promise = new Promise(function(resolve, reject) {
  // executor(生产者代码,“歌手”)
});
  • 传入的参数:一个被称为 executor 的函数,当 Promise 创建完成后,此函数自动运行,执行生产者代码
  • executor 参数:

    resolvereject 两个函数由引擎预先定义,无需自行创建,直接调用即可,两个函数只会成功调用一个,不存在同时被调用。

    • resolve(value) —— 如果任务成功完成并带有结果 value
    • reject(error) —— 如果出现了 errorerror 即为 Error 对象(允许使用其他非 Error 类型,但不建议)

Promise 对象内部属性,无法外部访问:

  • state —— 从 "pending",如果 resolve 被调用时变为 "fulfilled",否则 reject 被调用时变为 "rejected"
  • result —— 从 undefined,如果 resolve(value) 被调用时变为 value,否则 reject(error) 被调用时变为 error

Promise 最终的状态只能为二者之一,因此只能调用相对应状态的 resolve/reject 函数:

  1. state: "fulfilled"result: value
  2. state: "rejected"result: error

消费者:thencatch

Promise 只是存在于 executor 和消费函数之间的桥梁,可以使用 thencatch 注册消费函数。

then

语法:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve 运行 .then 中的第一个函数
promise.then(
  result => console.info(result), // 注册成功的消费函数
  error => console.info(error) // 注册失败的消费函数
);
  • resolved 时,自动运行成功消费函数,忽略失败消费函数
  • rejected 时,自动运行失败消费函数,忽略成功消费函数

catch

then 方法既可以注册成功函数,也可以注册失败函数,但允许只注册成功函数,然后在 catch 中注册失败函数。

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) 与 promise.then(null, f) 一样
promise.catch(console.info); // 1 秒后显示 "Error: Whoops!"

.catch(console.info) 相当于 .then(null, console.info)

finally 清理

支持在创建 Promise 对象后执行清理终结的动作,要注意:

  • finally 不传入参数,因此未知 Promise 是否执行成功
  • 即使 finally 返回内容也会被忽略
  • finally 抛出 error 时,转到最近的 error 处理程序中
new Promise((resolve, reject) => {
  throw new Error("error");
})
  .finally(() => console.info("Promise ready")) // 先触发
  .catch(err => console.info(err));  // <-- .catch 显示这个 error

Promise vs. 回调:异步编程模式对比

维度Promise回调
编码顺序先启动异步任务,通过 .then 处理结果,符合自然阅读顺序必须在调用异步函数就把回调准备好,顺序倒置
多次订阅同一次异步操作可多次 .then,每个处理函数独立加入“粉丝列表”一个回调只能被调用一次;如需多个处理逻辑需手动封装或层层嵌套
链式/组合返回新 Promise,可链式 .thenPromise.all/race 等组合需要手动管理回调嵌套或额外库,易形成“回调地狱”
错误处理统一 .catch 捕获整条链异常,支持 throwreject 自动传播每个回调需单独判断 err 参数,遗漏即静默失败
可读性扁平链式结构,逻辑清晰多层嵌套后缩进爆炸,维护困难

Promise 让异步代码先执行、后处理,且可多次订阅、可链式组合;回调则必须先处理、后执行,并受限于单次回调的嵌套噩梦。

Promise 链

适用于一个接一个调用异步任务场景。

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  console.info(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  console.info(result); // 2
  return result * 2;

}).then(function(result) {

  console.info(result); // 4
  return result * 2;

});
  • then 的回调若返回非 Promise 值,JavaScript 会把它自动包装成立即 resolvedPromise,再传给下一环;
  • 只有返回真正的 Promise 时才会等待其状态。因此链式调用始终成立,无需手动 return Promise.resolve(x)

显式返回 Promise

对于真正的 Promise,下一个 resolved 会等待其状态

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  console.info(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  console.info(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  console.info(result); // 4

});

Thenables

"thenable" 对象:一个具有方法 .then 的任意对象,会被当做一个 Promise 来对待。

因此三方库可以实现一个与 Promise 相兼容的对象,也即是实现自己的 then 方法。

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    console.info(resolve); // function() { native code }
    // 1 秒后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    // 引擎会对返回的对象进行检查
    // 若存在 then 方法即会进行调用
    // 并提供原生的函数 resolve 和 reject 作为参数
    return new Thenable(result); // (*)
  })
  .then(console.info); // 1000ms 后显示 2

Promise 错误处理

当一个 Promisereject 时,控制权将移交至最近的 rejection 处理程序。

比如将 Promisecatch 方法附在链尾:

需要 v18 版本的 Node.js 环境才原生支持 fetch,或者使用浏览器

fetch('https://no-such-server.blabla ') // reject
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/ ${user.name}`))
  .then(response => response.json())
  .catch(err => console.info(err))

隐式捕获

执行者(executor)和 Promise 的处理程序周围有一个“隐式的 try..catch”,因此

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(console.info); // Error: Whoops!

等同于

new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(console.info); // Error: Whoops!

因此会自动捕获了 error,并将其变为 rejected Promise,然后将错误移至最近的错误处理程序。

再次抛出

try..catch 一样,catch 方法中允许再次将未知的错误抛出。每一次错误都会转到最近的处理程序,处理完成后转到最近的 then 方法。

// 执行流:catch -> catch
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) { // (*)

  if (error instanceof URIError) {
    // 处理它
  } else {
    console.info("Can't handle such error");

    throw error; // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
  }

}).then(function() {
  /* 不在这里运行 */
}).catch(error => { // (**)

  console.info(`The unknown error has occurred: ${error}`);
  // 不会返回任何内容 => 执行正常进行

}).then(function() {
  /* 继续运行 */
});

未处理的 rejection

  • 出现 errorPromise 状态变为 rejected,执行最近的 rejection 处理程序
  • 若未找到最近的 rejection 处理程序,引擎会跟踪此类 rejection
  • 之后生成全局 error

浏览器中可以注册事件进行监听此类错误:

window.addEventListener('unhandledrejection', function(event) {
  // 这个事件对象有两个特殊的属性:
  console.info(event.promise); // [object Promise] —— 生成该全局 error 的 promise
  console.info(event.reason); // Error: Whoops! —— 未处理的 error 对象
});

new Promise(function() {
  throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch

Promise API

所有 API 都返回一个 Promise

Promise.all(promises)

并行等待所有 Promise 完成,全部成功返回结果数组(按原顺序),任一失败立即拒绝。

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)),
  new Promise(resolve => setTimeout(() => resolve(2), 2000)),
  new Promise(resolve => setTimeout(() => resolve(3), 1000))
]).then(console.info); // 1,2,3(按原顺序,不受结束时间影响)

// 常见技巧:数据数组 → Promise 数组 → Promise.all
let urls = [
  'https://api.github.com/users/iliakan ',
  'https://api.github.com/users/remy ',
  'https://api.github.com/users/jeresig '
];

let requests = urls.map(url => fetch(url));

Promise.all(requests)
  .then(responses => responses.forEach(
    response => console.info(`${response.url}: ${response.status}`)
  ))
  .catch(err => console.info(err));

// 任一失败,整体立即拒绝
Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(console.info); // Error: Whoops!

一旦有一个 Promise 变成 rejected,则整体立即 rejected,即使其他 Promise 仍在执行,但不再关心其状态。

Promise.allSettled(promises)

等待所有 Promisesettle(无论成败),返回状态对象数组。

/**
 * 结果格式:
 * - 成功: {status:"fulfilled", value:result}
 * - 失败: {status:"rejected", reason:error}
 */

let urls = [
  'https://api.github.com/users/iliakan ',
  'https://api.github.com/users/remy ',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => {
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        console.info(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        console.info(`${urls[num]}: ${result.reason}`);
      }
    });
  });

Promise.race(promises)

只等待第一个 settledPromise,无论成功或失败。

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.info); // 1

Promise.any(promises)

只等待第一个 fulfilledPromise;若全部失败,返回 AggregateError

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(console.info); // 1

// 全部失败
Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000))
]).catch(error => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors[0]);        // Error: Ouch!
});

Promise.resolve(value)

将值包装为立即成功的 Promise,用于兼容性处理。

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // 同步值 → Promise
  }
  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache.set(url, text);
      return text;
    });
}

Promise.reject(error)

创建立即失败的 Promise(极少使用)。

Promise.reject(new Error("fail")).catch(console.info); // Error: fail

对比速查

方法成功条件失败条件返回值/行为
Promise.all全部成功任一失败结果数组 / 首个错误
Promise.allSettled全部落定无(永不拒绝)状态对象数组
Promise.race首个落定首个落定(若失败)首个结果/错误
Promise.any首个成功全部失败首个结果 / AggregateError
Promise.resolve立即-成功的 Promise
Promise.reject-立即失败的 Promise

Promisification

指将一个接受回调的函数转换为一个返回 Promise 的函数。由于历史原因,许多库函数都是基于回调,因此 Promise 化是有意义的。但不是回调的完全替代。

Promise 装饰器:

// promisify(f, true) 来获取结果数组
function promisify(f, manyArgs = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...results) { // 我们自定义的 f 的回调
        if (err) {
          reject(err);
        } else {
          // 如果 manyArgs 被指定,则使用所有回调的结果 resolve
          resolve(manyArgs ? results : results[0]);
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
}

// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);
  • promisify(f) 的形式调用时,返回正常的 Promise 化的函数
  • promisify(f, true) 的形式调用时,返回以回调函数数组为结果 resolvePromise

在 Node.js 中,有一个内建的 Promise 化函数 util.promisify

请记住,一个 Promise 可能只有一个结果,但从技术上讲,一个回调可能被调用很多次。

因此,promisification 仅适用于调用一次回调的函数。进一步的调用将被忽略。

微任务

微任务队列

promise 的处理程序 .then、.catch 和 .finally 都是异步的,
这三个方法内部的代码会晚于其余正常代码的执行。

let promise = Promise.resolve();

promise.then(() => console.info("promise done!")); // 后显示

console.info("code finished"); // 先显示

内部队列 PromiseJobs(微任务队列):

  • 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
  • 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。

promise 准备就绪后, .then、.catch 和 .finally会被放入微任务队列中等待执行。

未处理的 rejection

如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。

如果延迟catch方法的处理时间(浏览器中):

let promise = Promise.reject(new Error("Promise Failed!"));
// 再次被catch捕获
setTimeout(() => promise.catch(err => console.info('caught')), 1000);

// 会先被全局捕获
// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => console.info(event.reason));

引擎会检查 promise:

  • 如果 promise 中的任意一个出现 “rejected” 状态,unhandledrejection 事件就会被触发
  • 由于已经添加了catch方法,1000ms后,被添加到 setTimeout 中的 .catch 也会被触发,对捕获的错误进行显示

async/await

async

允许对函数进行修饰,使其总是返回一个 resolved 的 promise

async function f() {
  return 1;
}

f().then(console.info); // 1

等同于显式的返回一个Promise

async function f() {
  return Promise.resolve(1);
}

f().then(console.info); // 1

await

让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果

只在 async 函数内工作

// 只在 async 函数内工作
let value = await promise;

示例:

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // 等待,直到 promise resolve (*)

  console.info(result); // "done!"
}

f();
  • 执行到 (*) 行暂停,在 promise settle 时,拿到 result 作为结果继续往下执行
  • await 实际上会暂停函数的执行,直到 promise 状态变为 settled
  • 然后以 promise 的结果继续执行

这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务

await接受"thenables"

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    console.info(resolve);
    // 1000ms 后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // 等待 1 秒,之后 result 变为 2
  let result = await new Thenable(1);
  console.info(result);
}

f();

在类中定义async方法:

class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(console.info); // 1(console.info 等同于 result => console.info(result))

Error 处理

await所期望的Promise被reject时,相当于throw一个Error,之后正常try..catch捕获以及再次抛出即可。

或者在Promise后增加catch方法进行捕获

async function f() {
  await Promise.reject(new Error("Whoops!"));
}

等同于

async function f() {
  throw new Error("Whoops!");
}

async、await与then、catch

  • 使用 async/await 时,几乎不会用到 .then,因为await 处理了等待
  • 使用常规的 try..catch 代替使用 .catch
  • 在代码的顶层时,也就是在所有 async 函数之外,在语法上就不能使用 await 了,此时需要添加 .then/catch 来处理最终的结果或者错误

async/await 与 Promise.all

需要同时等待多个 promise 时,可以用 Promise.all 把它们包装起来,然后使用 await

let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);