Skip to content

promise的使用

基本上可以这么说,promise是一个返回给你的对象,你可以往这个对象上追加一些回调函数,从而避免了以前那种往函数里塞回调函数(callback)的写法。 比如,以前你可能写过类似这样的旧回调风格代码:

js
function successCallback(result) {
  console.log("It succeeded with " + result);
}

function failureCallback(error) {
  console.log("It failed with " + error);
}

doSomething(successCallback, failureCallback);

用promise的话,你可以这么写:

js
let promise = doSomething();
promise.then(successCallback, failureCallback);

甚至可以这么写:

js
doSomething().then(successCallback, failureCallback);

我们称之为异步函数调用(asynchronous function call)。这种代码风格有一些优点,下面我们逐一说明。

一些保证

不像旧回调风格代码,promise可以确保下面这些特性:

  • 回调不会在当前事件循环结束之前被调用;
  • 通过.then追加的回调函数在异步操作结束(成功或失败)之后会被调用,即便这个.then是在异步操作已经被resolved之后追加的;
  • 可以通过多次追加.then来添加多个回调函数,这些回调函数会按照被添加的顺序独立执行。

不过promise的最直接的好处还是可链式使用。

链式使用

有一个常见的需求是连续执行两个或者多个异步操作,每个后续操作都在前一个操作成功后才开始执行,并且后续操作可以收到前一个成功操作返回的数据。我们可以通过promise链来实现这个需求。 敲黑板,划重点:这里的重点在于让.then方法返回一个不同于前面的、新的promise。

js
let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);

或者这样写:

js
let promise2 = doSomething().then(successCallback, failureCallback);

这里的promise2不仅仅表示doSomething()的完成,也表示了传入的successCallback或failureCallback的完成——这里的successCallback和failureCallback可以是其他会返回promise的异步函数。如果这里的successCallback、failureCallback是会返回promise的异步函数的话,追加到promise2上的回调函数就会在successCallback或failureCallback执行完后再被调用。 基本上,在由promise构成的链上,每个promise都表示另一个异步操作的结束。 以前,要在一起写好多个异步操作代码很容易出现经典的“回调金字塔厄运”(callback pyramid of doom):

js
doSomething(function (result) {
  doSomethingElse(
    result,
    function (newResult) {
      doThirdThing(
        newResult,
        function (finalResult) {
          console.log("Got the final result: " + finalResult);
        },
        failureCallback,
      );
    },
    failureCallback,
  );
}, failureCallback);

但是现在,我们可以把回调函数追加到返回的promise上,形成一个promise链,就像这样:

js
doSomething()
  .then(function (result) {
    return doSomethingElse(result);
  })
  .then(function (newResult) {
    return doThirdThing(newResult);
  })
  .then(function (finalResult) {
    console.log("Got the final result: " + finalResult);
  })
  .catch(failureCallback);

敲黑板,划重点:then方法里的参数是可选的。另外,catch(failureCallback)then(null, failureCallback) 的简写形式。如果使用箭头函数,上面的代码可以进一步简写为:

js
doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

重点提示:总是要返回promise,否则这些回调函数就没办法链在一起了,错误也将没法被捕获。

catch之后的链

在失败(catch)之后也是可以继续链下去的,这可以让你在即使链中的某个行为失败了也可以继续完成新的行为。下面是一个示例代码:

js
new Promise((resolve, reject) => {
  console.log("Initial");

  resolve();
})
  .then(() => {
    throw new Error("Something failed");

    console.log("Do this");
  })
  .catch(() => {
    console.log("Do that");
  })
  .then(() => {
    console.log("Do this whatever happened before");
  });

上面的代码执行后会输入下面的内容:

js
Initial
Do that
Do this whatever happened before

注意,这里“Do this”不会被打印出来,因为"Something failed"这个错误被抛出时会触发一个rejection。

错误传播

还记得前文我们提到“回调金字塔厄运”(callback pyramid of doom)时举例用的代码吗?那块金字塔代码里,failureCallback 一共出现了三次。但是在promise链里failureCallback只需要在最底部出现一次:

js
doSomething()
  .then((result) => doSomethingElse(value))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => console.log(`Got the final result: ${finalResult}`))
  .catch(failureCallback);

基本上,如果promise链中出现了一个异常,这个链就会停止后续的动作,取而代之的是会沿着链往下找绑定catch处理代码块。这很像同步代码的书写模型:

js
try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch (error) {
  failureCallback(error);
}

ECMSScript 2017引入了async/await语法糖来实现类似的同步代码书写风格:

js
async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch (error) {
    failureCallback(error);
  }
}

async/await语法糖是建立在promise的肩膀之上的,上述代码片段里的doSomething 跟前面的函数是一样的(返回promise)。 Promise通过捕获所有的错误(包括抛出的异常和程序性错误)初步解决了回调函数金字塔的问题。这对异步操作的代码书写而言是非常有必要的。

将旧API包装为Promise

Promise可以通过对应的构造器非常方便的创建出来。但有些旧的API,需要我们自行将其包裹为一个promise。 在一个理想的代码国度里,所有异步函数应该都会返回promise。唉,但是,一些API仍旧期望使用者按以前的方式传入成功和/或失败回调。一个典型的例子就是setTimeout()函数:

js
setTimeout(() => saySomething("10 seconds passed"), 10000);

混合使用旧式回调和promise无异于自讨没趣。如果saySomething 失败了或者包含有语法性错误,并没有相应的代码来捕获它们。 幸运的是,我们可以将它包装为一个promise。这里有个最佳实践,就是尽可能把容易出问题的函数包装在最底层,然后从不直接调用它们:

js
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait(10000)
  .then(() => saySomething("10 seconds"))
  .catch(failureCallback);

基本上,promise构造器接受一个执行函数,在这个执行函数里我们可以手动对promise进行resolve或reject。因为setTimeout并不会真的“失败”,所以在上面的这个例子里,我们省略了promise被reject的情况。

成员

Promise.resolve()Promise.reject() 是创建已经被resolved或rejected的promise对象的简便写法。有时这会很方便。 Promise.all()Promise.race() 可用于并行执行多个异步操作。 顺序执行多个异步操作可以借助下面这样“聪明”的代码实现:

js
[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

上面的代码可以这么理解,我们把一个由异步函数组成的数组通过数组的reduce方法拼接成了一个promise链,它等价于:Promise.resolve().then(func1).then(func2); 。 这个效果也可以通过一个可重复使用的compose函数来实现——这在函数式编程中是非常常见的:

js
let applyAsync = (acc, val) => acc.then(val);
let composeAsync =
  (...funcs) =>
  (x) =>
    funcs.reduce(applyAsync, Promise.resolve(x));

composeAsync函数会接受任意数量的函数作为入参,然后返回一个新的函数,这个新的函数会接收一个初始值。

js
let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

在ECMAScript 2017中,这个顺序执行的需求可以借助async/await更简单地实现:

js
for (let f of [func1, func2]) {
  await f();
}

时序

不要惊讶,传给then的函数从来都不会被同步调用,即便是在promise都已经被resolved的情况下,比如下面这样的代码,先打印出来的是1,然后才是2,尽管打印2的代码在打印1的代码之前出现:

js
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

在上面这个示例代码里,被传入then的函数不会立即被执行,而是会被堆放在一个微任务队列(microtask queue)上——这意味着这个被传入的函数会在当前JavaScript事件循环结束后微任务队列被清空时执行,反正也是相当快就会被执行到的。 下面是另一个例子:

js
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

参考:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using\_promises

天上的神明与星辰,人间的艺术和真纯,我们所敬畏和热爱的,莫过于此。