外观
基本上可以这么说,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可以确保下面这些特性:
不过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)之后也是可以继续链下去的,这可以让你在即使链中的某个行为失败了也可以继续完成新的行为。下面是一个示例代码:
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通过捕获所有的错误(包括抛出的异常和程序性错误)初步解决了回调函数金字塔的问题。这对异步操作的代码书写而言是非常有必要的。
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, 4https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using\_promises