虽然 JS 是异步执行的语言,但是人的思维是同步的————因此,开发者总是在寻求如何使用逻辑上看似同步的代码来完成 JS 的异步请求。目前,开发人员已经给出了解决异步的成熟可行的方案,那就是Promise,并进一步提出了基于Promise的async函数。
Promise
Promise可以实现其他语言中类似Future和Deferred一样的功能,它既可以像事件和回调函数一样指定稍后执行的代码,也可以明确指示代码是否成功执行。基于这些成功或失败的状态,决定代码是否继续执行。为了让代码更容易理解和调试,支持链式地编写Promise。
Promise的生命周期
每个Promise都会经历一个短暂的生命周期:先是处于进行中(pending)的状态,此时操作尚未完成,所以它也是未处理(unsettled)的;一旦异步操作执行结束,Promise则变为已处理(settled)的状态,fulfilled标志Promise异步操作成功完成,rejected标志由于程序错误或一些其他原因,Promise异步操作未能成功。
内部属性[[PromiseState]]被用来表示Promise的3种状态:pending、fulfilled及rejected。这个属性不暴露在Promise对象上,所以不能以编程的方式检测Promise的状态,当Promise的状态改变时,then()方法会采取特定的行动。
then
所有Promise都有then()方法,它接受两个参数:
第一个是当Promise的状态变为fulfilled时要调用的函数,与异步操作相关的附加数据都会传递给这个完成函数。
第二个是当Promise的状态变为rejected时要调用的函数,其与完成时调用的函数类似,所有与失败状态相关的附加数据都会传递给这个拒绝函数。
then()的两个参数都是可选的,所以可以按照任意组合的方式来监听Promise,执行完成或被拒绝都会被响应。
let promise = readFile("example.txt");
promise.then(function(contents) { // 同时监听了执行完成和执行被拒;
// 完成
console.log(contents);
}, function(err) {
// 拒绝
console.error(err.message);
});
promise.then(function(contents) { // 只监听了执行完成,错误时不报告
// 完成
console.log(contents);
});
promise.then(null, function(err) { // 只监听了执行被拒,成功时不报告
// 拒绝
console.error(err.message);
});
如果一个对象实现了上述的then()方法,那这个对象我们称之为thenable对象。所有的Promise都是thenable对象,但并非所有thenable对象都是Promise。
Promise还有一个catch()方法,相当于只给其传入拒绝处理程序的then()方法。
promise.catch(function(err) {
// 拒绝
console.error(err.message);
});
// 等同于:
promise.then(null, function(err) {
// 拒绝
console.error(err.message);
});
如果一个Promise处于己处理状态,在这之后添加到任务队列中的处理程序仍将执行。所以无论何时都可以添加新的完成处理程序或拒绝处理程序,同时也可以保证这些处理程序能被调用。
let promise = readFile("example.txt");
// 原始的完成处理函数
promise.then(function(contents) {
console.log(contents);
// 现在添加另一个
promise.then(function(contents) {
console.log(contents);
});
});
创建未处理的Promise
用Promise构造函数可以创建新的Promise,构造函数只接受一个参数:包含初始化Promise代码的执行器(executor)函数。执行器接受两个参数,分别是resolve()函数和reject()函数。执行器成功完成时调用resolve()函数,反之,失败时则调用reject()函数。
// Node.js 范例
let fs = require("fs");
function readFile(filename) {
return new Promise(function(resolve, reject) {
// 触发异步操作
fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {
// 检查错误
if (err) {
reject(err);
return;
}
// 读取成功
resolve(contents);
});
});
}
let promise = readFile("example.txt");
// 同时监听完成与拒绝
promise.then(function(contents) {
// 完成
console.log(contents);
}, function(err) {
// 拒绝
console.error(err.message);
});
readFile()方法被调用时执行器会立刻执行,在执行器中,无论是调用resolve()还是reject(),都会向任务队列中添加一个任务来解决这个Promise。
let promise = new Promise(function(resolve, reject) {
console.log("Promise");
resolve();
});
console.log("Hi!");
这段代码的输出内容是
promise
Hi !
调用resolve()后会触发一个异步操作,传入then()和catch()方法的函数会被添加到任务队列中并异步执行。
let promise = new Promise(function(resolve, reject) {
console.log("Promise");
resolve();
});
promise.then(function() {
console.log("Resolved.");
});
console.log("Hi!");
这个示例的输出内容为
promise
Hi !
Resolved
即使在代码中then()调用位于console.log("Hi!")之前,但其与执行器不同,它并没有立即执行。这是因为,完成处理程序和拒绝处理程序总是在执行器完成后被添加到任务队列的末尾。
创建已处理的Promise
创建未处理Promise的最好方法是使用Promise的构造函数,这是由于Promise执行器具有动态性。但如果想用Promise来表示一个已知值,则编排一个只是简单地给resolve()函数传值的任务并无实际意义,反倒是可以用以下两种方法根据特定的值来创建己解决Promise。
Promise.resolve()
方法只接受一个参数并返回一个完成态的Promise,也就是说不会有任务编排的过程,而且需要向Promise添加一至多个完成处理程序来获取值。
let promise = Promise.resolve(42);
promise.then(function(value) {
console.log(value); // 42
});
同理也可以通过Promise.reject()
方法来创建已拒绝态的Promise,它与Promise.resolve()
很像,唯一的区别是创建出来的是拒绝态的Promise。
非Promise的Thenable对象
Promise.resolve()方法和Promise.reject()方法都可以接受非Promise的Thenable对象作为参数。如果传入一个非Promise的Thenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
在此示例中,Promise.resolve()调用的是thenable.then(),所以Promise的状态可以被检测到。由于是在then()方法内部调用了resolve(42),因此thenable对象的Promise状态是已完成。新创建的已完成状态Promise p1从thenable对象接受传入的值(也就是42),p1的完成处理程序将42赋值给形参value。
执行器错误
如果执行器内部抛出一个错误,则Promise的拒绝处理程序就会被调用。
let promise = new Promise(function(resolve, reject) {
throw new Error("Explosion!");
});
promise.catch(function(error) {
console.log(error.message); // "Explosion!"
});
串联
每次调用then()方法或catch()方法时实际上创建并返回了另一个Promise,只有当第一个Promise完成或被拒绝后,第二个才会被解决。
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value);
}).then(function() {
console.log("Finished");
});
这段代码输出以下内容:
42
Finished
捕获错误
在之前的示例中,完成处理程序或拒绝处理程序中可能发生错误,而Promise链可以用来捕获这些错误。
let p1 = new Promise(function(resolve, reject) {
throw new Error("Explosion!");
});
p1.catch(function(error) {
console.log(error.message); // "Explosion!"
throw new Error("Boom!");
}).catch(function(error) {
console.log(error.message); // "Boom!"
});
链式Promise调用可以感知到链中其他Promise的错误。
Promise链的返回值
Promise链的另一个重要特性是可以给下游Promise传递数据,已经知道了从执行器resolve()处理程序到Promise完成处理程序的数据传递过程,如果在完成处理程序中指定一个返回值,则可以沿着这条链继续传递数据。
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value); // "42"
return value + 1;
}).then(function(value) {
console.log(value); // "43"
});
在拒绝处理程序中也可以做相同的事情,当它被调用时可以返回一个值,然后用这个值完成链条中后续的Promise。
let p1 = new Promise(function(resolve, reject) {
reject(42);
});
p1.catch(function(value) {
// 第一个完成处理函数
console.log(value); // "42"
return value + 1;
}).then(function(value) {
// 第二个完成处理函数
console.log(value); // "43"
});
在Promise链中返回Promise
在Promise间可以通过完成和拒绝处理程序中返回的原始值来传递数据,但如果返回的是Promise对象,会通过一个额外的步骤来确定下一步怎么走。
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
p1.then(function(value) {
// 第一个完成处理函数
console.log(value); // 42
return p2;
}).then(function(value) {
// 第二个完成处理函数
console.log(value); // 43
});
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
p1.then(function(value) {
// 第一个完成处理函数
console.log(value); // 42
return p2;
}).catch(function(value) {
// 拒绝处理函数
console.log(value); // 43
});
Promise.all()
在Promise.all()
方法只接受一个参数并返回一个Promise,该参数是一个含有多个受监视Promise的可迭代对象(如一个数组),只有当可迭代对象中所有Promise都被解决后返回的Promise才会被解决,只有当可迭代对象中所有Promise都被完成后返回的Promise才会被完成。
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});
所有传入Promise.all()方法的Promise只要有一个被拒绝,那么返回的Promise没等所有Promise都完成就立即被拒绝。
Promise.race()
Promise.race()
方法监听多个Promise的方法稍有不同:它也接受含多个受监视Promise的可迭代对象作为唯一参数并返回一个Promise,但只要有一个Promise被解决返回的Promise就被解决,无须等到所有Promise都被完成。一旦数组中的某个Promise被完成,Promise.race()方法也会像Promise.all()方法一样返回一个特定的Promise。
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {
console.log(value); // 42
});
Generator
在 ES6 出现之前,基本都是各式各样类似Promise
的解决方案来处理异步操作的代码逻辑,ES6 的Generator
却给异步操作又提供了新的思路,马上就有人给出了如何用Generator
来更加优雅的处理异步操作,但Generator函数的执行必须靠执行器,要配合co模块使用,而ES2017给出了另一个更优雅的解决方案async函数。
async 函数是 Generator 函数的语法糖,该函数自带执行器,拥有更好的语义、更广的适用性。async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值),async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。
总之,在解决JS异步的问题上,相比于async,Generato显得既笨拙又复杂,本文将不再讨论Generator的使用。
async函数
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
上面代码指定50毫秒以后,输出hello world。
async 函数有多种使用形式
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
语法
async函数返回一个 Promise 对象,async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
Promise 对象的状态变化
async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2018 Language Specification"
上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log。
await命令
正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。
async function f() {
return await 123;
}
f().then(v => console.log(v)) // 123
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))// 出错了
只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
有时,希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}