Faster, Higher, Stronger
更快,更高,更强

promise的使用

基本上可以这么说,promise是一个返回给你的对象,你可以往这个对象上追加一些回调函数,从而避免了以前那种往函数里塞回调函数(callback)的写法。

比如,以前你可能写过类似这样的旧回调风格代码:

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

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

doSomething(successCallback, failureCallback);

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

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

甚至可以这么写:

doSomething().then(successCallback, failureCallback);

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

一些保证

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

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

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

链式使用

有一个常见的需求是连续执行两个或者多个异步操作,每个后续操作都在前一个操作成功后才开始执行,并且后续操作可以收到前一个成功操作返回的数据。我们可以通过promise链来实现这个需求。

敲黑板,划重点:这里的重点在于让.then方法返回一个不同于前面的、新的promise。

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

或者这样写:

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):

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

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

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) 的简写形式。如果使用箭头函数,上面的代码可以进一步简写为:

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

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

catch之后的链

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

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');
});

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

Initial
Do that
Do this whatever happened before

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

错误传播

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

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

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

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语法糖来实现类似的同步代码书写风格:

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()函数:

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

混合使用旧式回调和promise无异于自讨没趣。如果saySomething 失败了或者包含有语法性错误,并没有相应的代码来捕获它们。

幸运的是,我们可以将它包装为一个promise。这里有个最佳实践,就是尽可能把容易出问题的函数包装在最底层,然后从不直接调用它们:

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() 可用于并行执行多个异步操作。

顺序执行多个异步操作可以借助下面这样“聪明”的代码实现:

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

上面的代码可以这么理解,我们把一个由异步函数组成的数组通过数组的reduce方法拼接成了一个promise链,它等价于:Promise.resolve().then(func1).then(func2); 。

这个效果也可以通过一个可重复使用的compose函数来实现——这在函数式编程中是非常常见的:

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

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

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

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

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

时序

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

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

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

下面是另一个例子:

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

赞(0) 打赏
未经允许不得转载:峰间的云 » promise的使用

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏