pending
: 初始状态, 非 fulfilled
或 rejected
.html
fulfilled
: 成功的操做.git
rejected
: 失败的操做.github
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操做成功 */){
resolve(value);
} else {
reject(error);
}
});
复制代码
resolve函数的做用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操做成功时调用,并将异步操做的结果,做为参数传递出去;reject函数的做用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操做失败时调用,并将异步操做报出的错误,做为参数传递出去。 Promise实例生成之后,能够用then方法分别指定resolved状态和rejected状态的回调函数。面试
promise.then(function(value) {
// success
}, function(error) {
// failure
});
复制代码
它的做用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。shell
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function funcA(comments) {
console.log("resolved: ", comments);
}, function funcB(err){
console.log("rejected: ", err);
});
复制代码
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。所以能够采用链式写法,即then方法后面再调用另外一个then方法。json
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。api
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
复制代码
finally方法用于指定无论 Promise 对象最后状态如何,都会执行的操做。数组
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
复制代码
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。promise
const p = Promise.all([p1, p2, p3]);
bash
上面代码中,Promise.all方法接受一个数组做为参数,p一、p二、p3都是 Promise 实例,若是不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数能够不是数组,但必须具备 Iterator 接口,且返回的每一个成员都是 Promise 实例。)
p的状态由p一、p二、p3决定,分红两种状况。
(1)只有p一、p二、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p一、p二、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p一、p二、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
复制代码
const p = Promise.race([p1, p2, p3]);
复制代码
上面代码中,只要p一、p二、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
有时须要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个做用。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
复制代码
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
复制代码
使用其反作用而不是return 下面的代码有什么问题?
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// Gee, I hope someOtherPromise() has resolved!
// Spoiler alert: it hasn't.
});
复制代码
每个promise对象都会提供一个then方法或者是catch方法
somePromise().then(function () {
// I'm inside a then() function!
});
复制代码
咱们在这里能作什么呢?有三种事能够作:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
复制代码
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
});
复制代码
函数什么都不返回等于返回了 undefined 目前为止,咱们看到给 .then() 传递的都是函数,可是其实它能够接受非函数值:
later(1000)
.then(later(2000))
.then(function(data) {
// data = later_1000
});
复制代码
给 .then() 传递非函数值时,实际上会被解析成 .then(null),从而致使上一个 promise 对象的结果被“穿透”。因而,上面的代码等价于:
later(1000)
.then(null)
.then(function(data) {
// data = later_1000
});
复制代码
为了不没必要要的麻烦,建议老是给 .then() 传递函数。
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // throwing a synchronous error!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
}).catch(function (err) {
// Boo, I got an error!
});
复制代码
cacth()和then(null, …)并不彻底相同
下面两个代码是不等价的,当使用then(resolveHandler, rejectHandler),rejectHandler不会捕获在resolveHandler中抛出的错误。
somePromise().then(function () {
return someOtherPromise();
}).catch(function (err) {
// handle error
});
somePromise().then(function () {
return someOtherPromise();
}, function (err) {
// handle error
});
复制代码
对于每一个promise对象来讲,一旦它被建立,相关的异步代码就开始执行了
promise坠落现象 这个错误我在前文中提到的问题中间接的给出了。这个状况比较深奥,或许你永远写不出这样的代码,可是这种写法仍是让笔者感到震惊。 你认为下面的代码会输出什么?
Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) {
console.log(result);
});
复制代码
若是你认为输出的是bar,那么你就错了。实际上它输出的是foo!
产生这样的输出是由于你给then方法传递了一个非函数(好比promise对象)的值,代码会这样理解:then(null),所以致使前一个promise的结果产生了坠落的效果。你能够本身测试一下:
Promise.resolve('foo').then(null).then(function (result) {
console.log(result);
});
复制代码
让咱们回到以前讲解promise vs promise factoriesde的地方。简而言之,若是你直接给then方法传递一个promise对象,代码的运行是和你所想的不同的。then方法应当接受一个函数做为参数。所以你应当这样书写代码:
Promise.resolve('foo').then(function () {
return Promise.resolve('bar');
}).then(function (result) {
console.log(result);
});
复制代码
function fetch (api, ms, err = false) {
return new Promise(function (resolve, reject) {
console.log(`fetch-${api}-${ms} start`)
setTimeout(function () {
err ? reject(`reject-${api}-${ms}`) : resolve(`resolve-${api}-${ms}`)
}, ms)
})
}
解法一
function loadData () {
const promises = [fetch('API1', 3000), fetch('API2', 2000), fetch('API3', 5000)]
promises.reduce((chain, promise) => {
return chain.then((res) => {
console.log(res)
return promise
})
}, Promise.resolve('haha')).then(res => {
console.log(res)
})
}
loadData()
// 解法二
async function loadData () {
const promises = [fetch('API1', 3000), fetch('API2', 2000), fetch('API3', 5000)]
for (const promise of promises) {
try {
await promise.then(res => console.log(res))
} catch (err) {
console.error(err)
}
}
}
复制代码
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
复制代码
输出结果为:1,2,4,3。
解题思路:then方法是异步执行的。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
reject('error')
}, 1000)
})
promise.then((res)=>{
console.log(res)
},(err)=>{
console.log(err)
})
复制代码
输出结果:success
解题思路:Promise状态一旦改变,没法在发生变动。
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
复制代码
输出结果:1
解题思路:Promise的then方法的参数指望是函数,传入非函数则会发生值穿透。
setTimeout(()=>{
console.log('setTimeout')
})
let p1 = new Promise((resolve)=>{
console.log('Promise1')
resolve('Promise2')
})
p1.then((res)=>{
console.log(res)
})
console.log(1)
复制代码
输出结果:
Promise1 1 Promise2 setTimeout
解题思路:这个牵扯到js的执行队列问题,整个script代码,放在了macrotask queue中,执行到setTimeout时会新建一个macrotask queue。可是,promise.then放到了另外一个任务队列microtask queue中。script的执行引擎会取1个macrotask queue中的task,执行之。而后把全部microtask queue顺序执行完,再取setTimeout所在的macrotask queue按顺序开始执行。(具体参考www.zhihu.com/question/36…)
setImmediate(function(){
console.log(1);
},0);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
process.nextTick(function(){
console.log(7);
});
console.log(8);
复制代码
结果是:3 4 6 8 7 5 2 1
复制代码
优先级关系以下:
process.nextTick > promise.then > setTimeout > setImmediate
复制代码
V8实现中,两个队列各包含不一样的任务:
macrotasks: script(总体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserver
复制代码
执行过程以下:JavaScript引擎首先从macrotask queue中取出第一个任务,执行完毕后,将microtask queue中的全部任务取出,按顺序所有执行;而后再从macrotask queue中取下一个,执行完毕后,再次将microtask queue中的所有取出;循环往复,直到两个queue中的任务都取完。
解释:代码开始执行时,全部这些代码在macrotask queue中,取出来执行之。后面遇到了setTimeout,又加入到macrotask queue中,而后,遇到了promise.then,放入到了另外一个队列microtask queue。等整个execution context stack执行完后,下一步该取的是microtask queue中的任务了。所以promise.then的回调比setTimeout先执行。 5.
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
return 3;
})
.then((res) => {
console.log(res);
});
复制代码
输出结果:1 2
解题思路:Promise首先resolve(1),接着就会执行then函数,所以会输出1,而后在函数中返回2。由于是resolve函数,所以后面的catch函数不会执行,而是直接执行第二个then函数,所以会输出2。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('开始');
resolve('success');
}, 5000);
});
const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});
promise.then((res) => {
console.log(res, Date.now() - start);
});
复制代码
输出结果:
开始
success 5002
success 5002
解题思路:promise 的**.then或者.catch能够被调用屡次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,而且有了一个值,那么后续每次调用.then** 或者**.catch**都会直接拿到该值。
let p1 = new Promise((resolve,reject)=>{
let num = 6
if(num<5){
console.log('resolve1')
resolve(num)
}else{
console.log('reject1')
reject(num)
}
})
p1.then((res)=>{
console.log('resolve2')
console.log(res)
},(rej)=>{
console.log('reject2')
let p2 = new Promise((resolve,reject)=>{
if(rej*2>10){
console.log('resolve3')
resolve(rej*2)
}else{
console.log('reject3')
reject(rej*2)
}
})
  return p2
}).then((res)=>{
console.log('resolve4')
console.log(res)
},(rej)=>{
console.log('reject4')
console.log(rej)
})
复制代码
输出结果:
reject1 reject2 resolve3 resolve4 12
解题思路:咱们上面说了Promise的先进之处在于能够在then方法中继续写Promise对象并返回。
new Promise(resolve => {
console.log(1);
resolve(3);
new Promise((resolve2 => {
resolve2(4)
})).then(res => {
console.log(res)
})
}).then(num => {
console.log(num)
});
console.log(2)
复制代码
输出1 2 4 3
9.重头戏!!!!实现一个简单的Promise
function Promise(fn){
var status = 'pending'
function successNotify(){
status = 'fulfilled'//状态变为fulfilled
toDoThen.apply(undefined, arguments)//执行回调
}
function failNotify(){
status = 'rejected'//状态变为rejected
toDoThen.apply(undefined, arguments)//执行回调
}
function toDoThen(){
setTimeout(()=>{ // 保证回调是异步执行的
if(status === 'fulfilled'){
for(let i =0; i< successArray.length;i ++) {
successArray[i].apply(undefined, arguments)//执行then里面的回掉函数
}
}else if(status === 'rejected'){
for(let i =0; i< failArray.length;i ++) {
failArray[i].apply(undefined, arguments)//执行then里面的回掉函数
}
}
})
}
var successArray = []
var failArray = []
fn.call(undefined, successNotify, failNotify)
return {
then: function(successFn, failFn){
successArray.push(successFn)
failArray.push(failFn)
return undefined // 此处应该返回一个Promise
}
}
}
复制代码
解题思路:Promise中的resolve和reject用于改变Promise的状态和传参,then中的参数必须是做为回调执行的函数。所以,当Promise改变状态以后会调用回调函数,根据状态的不一样选择须要执行的回调函数。
ES2017 标准引入了 async 函数,使得异步操做变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
前文有一个 Generator 函数,依次读取两个文件。
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
复制代码
写成async
函数,就是下面这样。
var asyncReadFile = async function () {
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
复制代码
一比较就会发现,async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
,仅此而已。
async
函数对 Generator 函数的改进,体如今如下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,因此才有了co
模块,而async
函数自带执行器。也就是说,async
函数的执行,与普通函数如出一辙,只要一行。
var result = asyncReadFile();
复制代码
上面的代码调用了asyncReadFile
函数,而后它就会自动执行,输出最后结果。这彻底不像 Generator 函数,须要调用next
方法,或者用co
模块,才能真正执行,获得最后结果。
(2)更好的语义。
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操做,await
表示紧跟在后面的表达式须要等待结果。
(3)更广的适用性。
co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,能够是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操做)。
(4)返回值是 Promise。
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你能够用then
方法指定下一步的操做。
进一步说,async
函数彻底能够看做多个异步操做,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
async
函数返回一个 Promise 对象,可使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操做完成,再接着执行函数体内后面的语句。
下面是一个例子。
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
复制代码
上面代码是一个获取股票报价的函数,函数前面的async
关键字,代表该函数内部有异步操做。调用该函数时,会当即返回一个Promise
对象。
下面是另外一个例子,指定多少毫秒后输出一个值。
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
函数返回的是 Promise 对象,能够做为await
命令的参数。因此,上面的例子也能够写成下面的形式。
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
复制代码
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
函数的语法规则整体上比较简单,难点是错误处理机制。
async
函数返回一个 Promise 对象。
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
复制代码
上面代码中,函数f
内部return
命令返回的值,会被then
方法回调函数接收到。
async
函数内部抛出错误,会致使返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
复制代码
async
函数返回的 Promise 对象,必须等到内部全部await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操做执行完,才会执行then
方法指定的回调函数。
下面是一个例子。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
复制代码
上面代码中,函数getTitle
内部有三个操做:抓取网页、取出文本、匹配页面标题。只有这三个操做所有完成,才会执行then
方法里面的console.log
。
正常状况下,await
命令后面是一个 Promise 对象。若是不是,会被转成一个当即resolve
的 Promise 对象。
async function f() {
return await 123;
}
f().then(v => console.log(v))
// 123
复制代码
上面代码中,await
命令的参数是数值123
,它被转成 Promise 对象,并当即resolve
。
await
命令后面的 Promise 对象若是变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到。
async function f() {
await Promise.reject('出错了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
复制代码
注意,上面代码中,await
语句前面没有return
,可是reject
方法的参数依然传入了catch
方法的回调函数。这里若是在await
前面加上return
,效果是同样的。
只要一个await
语句后面的 Promise 变为reject
,那么整个async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
复制代码
上面代码中,第二个await
语句是不会执行的,由于第一个await
语句状态变成了reject
。
有时,咱们但愿即便前一个异步操做失败,也不要中断后面的异步操做。这时能够将第一个await
放在try...catch
结构里面,这样无论这个异步操做是否成功,第二个await
都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
复制代码
另外一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
复制代码
若是await
后面的异步操做出错,那么等同于async
函数返回的 Promise 对象被reject
。
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
复制代码
上面代码中,async
函数f
执行后,await
后面的 Promise 对象会抛出一个错误对象,致使catch
方法的回调函数被调用,它的参数就是抛出的错误对象。具体的执行机制,能够参考后文的“async 函数的实现原理”。
防止出错的方法,也是将其放在try...catch
代码块之中。
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
} catch(e) {
}
return await('hello world');
}
复制代码
若是有多个await
命令,能够统一放在try...catch
结构中。
async function main() {
try {
var val1 = await firstStep();
var val2 = await secondStep(val1);
var val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}
复制代码
下面的例子使用try...catch
结构,实现屡次重复尝试。
const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
break;
} catch(err) {}
}
console.log(i); // 3
}
test();
复制代码
上面代码中,若是await
操做成功,就会使用break
语句退出循环;若是失败,会被catch
语句捕捉,而后进入下一轮循环。
第一点,前面已经说过,await
命令后面的Promise
对象,运行结果多是rejected
,因此最好把await
命令放在try...catch
代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另外一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
};
}
复制代码
第二点,多个await
命令后面的异步操做,若是不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
复制代码
上面代码中,getFoo
和getBar
是两个独立的异步操做(即互不依赖),被写成继发关系。这样比较耗时,由于只有getFoo
完成之后,才会执行getBar
,彻底可让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
复制代码
上面两种写法,getFoo
和getBar
都是同时触发,这样就会缩短程序的执行时间。
第三点,await
命令只能用在async
函数之中,若是用在普通函数,就会报错。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
复制代码
上面代码会报错,由于await
用在普通函数之中了。可是,若是将forEach
方法的参数改为async
函数,也有问题。
function dbFuc(db) { //这里不须要 async
let docs = [{}, {}, {}];
// 可能获得错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
复制代码
上面代码可能不会正常工做,缘由是这时三个db.post
操做将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for
循环。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
复制代码
若是确实但愿多个请求并发执行,可使用Promise.all
方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
复制代码
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
复制代码
全部的async
函数均可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。
下面给出spawn
函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
复制代码
咱们经过一个例子,来看 async 函数与 Promise、Generator 函数的比较。
假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。若是当中有一个动画出错,就再也不往下执行,返回上一个成功执行的动画的返回值。
首先是 Promise 的写法。
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
var ret = null;
// 新建一个空的Promise
var p = Promise.resolve();
// 使用then方法,添加全部动画
for(var anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});
}
复制代码
虽然 Promise 的写法比回调函数的写法大大改进,可是一眼看上去,代码彻底都是 Promise 的 API(then
、catch
等等),操做自己的语义反而不容易看出来。
接着是 Generator 函数的写法。
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});
}
复制代码
上面代码使用 Generator 函数遍历了每一个动画,语义比 Promise 写法更清晰,用户定义的操做所有都出如今spawn
函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn
函数就是自动执行器,它返回一个 Promise 对象,并且必须保证yield
语句后面的表达式,必须返回一个 Promise。
最后是 async 函数的写法。
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}
复制代码
能够看到Async函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将Generator写法中的自动执行器,改在语言层面提供,不暴露给用户,所以代码量最少。若是使用Generator写法,自动执行器须要用户本身提供。
实际开发中,常常遇到一组异步操做,须要按照顺序完成。好比,依次远程读取一组 URL,而后按照读取的顺序输出结果。
Promise 的写法以下。
function logInOrder(urls) {
// 远程读取全部URL
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// 按次序输出
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}
复制代码
上面代码使用fetch
方法,同时远程读取一组 URL。每一个fetch
操做都返回一个 Promise 对象,放入textPromises
数组。而后,reduce
方法依次处理每一个 Promise 对象,而后使用then
,将全部 Promise 对象连起来,所以就能够依次输出结果。
这种写法不太直观,可读性比较差。下面是 async 函数实现。
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
复制代码
上面代码确实大大简化,问题是全部远程操做都是继发。只有前一个URL返回结果,才会去读取下一个URL,这样作效率不好,很是浪费时间。咱们须要的是并发发出远程请求。
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
复制代码
上面代码中,虽然map
方法的参数是async
函数,但它是并发执行的,由于只有async
函数内部是继发执行,外部不受影响。后面的for..of
循环内部使用了await
,所以实现了按顺序输出。
《遍历器》一章说过,Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next
方法,就会获得一个对象,表示当前遍历指针所在的那个位置的信息。next
方法返回的对象的结构是{value, done}
,其中value
表示当前的数据的值,done
是一个布尔值,表示遍历是否结束。
这里隐含着一个规定,next
方法必须是同步的,只要调用就必须马上返回值。也就是说,一旦执行next
方法,就必须同步地获得value
和done
这两个属性。若是遍历指针正好指向同步操做,固然没有问题,但对于异步操做,就不太合适了。目前的解决方法是,Generator 函数里面的异步操做,返回一个 Thunk 函数或者 Promise 对象,即value
属性是一个 Thunk 函数或者 Promise 对象,等待之后返回真正的值,而done
属性则仍是同步产生的。
目前,有一个提案,为异步操做提供原生的遍历器接口,即value
和done
这两个属性都是异步产生,这称为”异步遍历器“(Async Iterator)。
异步遍历器的最大的语法特色,就是调用遍历器的next
方法,返回的是一个 Promise 对象。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
复制代码
上面代码中,asyncIterator
是一个异步遍历器,调用next
方法之后,返回一个 Promise 对象。所以,可使用then
方法指定,这个 Promise 对象的状态变为resolve
之后的回调函数。回调函数的参数,则是一个具备value
和done
两个属性的对象,这个跟同步遍历器是同样的。
咱们知道,一个对象的同步遍历器的接口,部署在Symbol.iterator
属性上面。一样地,对象的异步遍历器接口,部署在Symbol.asyncIterator
属性上面。无论是什么样的对象,只要它的Symbol.asyncIterator
属性有值,就表示应该对它进行异步遍历。
下面是一个异步遍历器的例子。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator
.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
});
复制代码
上面代码中,异步遍历器其实返回了两次值。第一次调用的时候,返回一个 Promise 对象;等到 Promise 对象resolve
了,再返回一个表示当前数据成员信息的对象。这就是说,异步遍历器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,做为中介。
因为异步遍历器的next
方法,返回的是一个 Promise 对象。所以,能够把它放在await
命令后面。
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
}
复制代码
上面代码中,next
方法用await
处理之后,就没必要使用then
方法了。整个流程已经很接近同步处理了。
注意,异步遍历器的next
方法是能够连续调用的,没必要等到上一步产生的Promise对象resolve
之后再调用。这种状况下,next
方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把全部的next
方法放在Promise.all
方法里面。
const asyncGenObj = createAsyncIterable(['a', 'b']);
const [{value: v1}, {value: v2}] = await Promise.all([
asyncGenObj.next(), asyncGenObj.next()
]);
console.log(v1, v2); // a b
复制代码
另外一种用法是一次性调用全部的next
方法,而后await
最后一步操做。
const writer = openFile('someFile.txt');
writer.next('hello');
writer.next('world');
await writer.return();
复制代码
前面介绍过,for...of
循环用于遍历同步的 Iterator 接口。新引入的for await...of
循环,则是用于遍历异步的 Iterator 接口。
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
复制代码
上面代码中,createAsyncIterable()
返回一个异步遍历器,for...of
循环自动调用这个遍历器的next
方法,会获得一个Promise对象。await
用来处理这个Promise对象,一旦resolve
,就把获得的值(x
)传入for...of
的循环体。
for await...of
循环的一个用途,是部署了 asyncIterable 操做的异步接口,能够直接放入这个循环。
let body = '';
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
复制代码
上面代码中,req
是一个 asyncIterable 对象,用来异步读取数据。能够看到,使用for await...of
循环之后,代码会很是简洁。
若是next
方法返回的Promise对象被reject
,那么就要用try...catch
捕捉。
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
复制代码
注意,for await...of
循环也能够用于同步遍历器。
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b
复制代码
就像 Generator 函数返回一个同步遍历器对象同样,异步 Generator 函数的做用,是返回一个异步遍历器对象。
在语法上,异步 Generator 函数就是async
函数与 Generator 函数的结合。
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
复制代码
上面代码中,异步操做前面使用await
关键字标明,即await
后面的操做,应该返回Promise对象。凡是使用yield
关键字的地方,就是next
方法的停下来的地方,它后面的表达式的值(即await file.readLine()
的值),会做为next()
返回对象的value
属性,这一点是于同步Generator函数一致的。
能够像下面这样,使用上面代码定义的异步Generator函数。
for await (const line of readLines(filePath)) {
console.log(line);
}
复制代码
异步 Generator 函数能够与for await...of
循环结合起来使用。
async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
复制代码
yield
命令依然是马上返回的,可是返回的是一个Promise对象。
async function* asyncGenerator() {
console.log('Start');
const result = await doSomethingAsync(); // (A)
yield 'Result: '+ result; // (B)
console.log('Done');
}
复制代码
上面代码中,调用next
方法之后,会在B
处暂停执行,yield
命令马上返回一个Promise对象。这个Promise对象不一样于A
处await
命令后面的那个 Promise 对象。主要有两点不一样,一是A
处的Promise对象resolve
之后产生的值,会放入result
变量;二是B
处的Promise对象resolve
之后产生的值,是表达式'Result: ' + result
的值;二是A
处的 Promise 对象必定先于B
处的 Promise 对象resolve
。
若是异步 Generator 函数抛出错误,会被 Promise 对象reject
,而后抛出的错误被catch
方法捕获。
async function* asyncGenerator() {
throw new Error('Problem!');
}
asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!
复制代码
注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步Iterator对象。基本上,能够这样理解,async
函数和异步 Generator 函数,是封装异步操做的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者经过for await...of
执行,或者本身编写执行器。下面就是一个异步 Generator 函数的执行器。
async function takeAsync(asyncIterable, count=Infinity) {
const result = [];
const iterator = asyncIterable[Symbol.asyncIterator]();
while (result.length < count) {
const {value,done} = await iterator.next();
if (done) break;
result.push(value);
}
return result;
}
复制代码
上面代码中,异步Generator函数产生的异步遍历器,会经过while
循环自动执行,每当await iterator.next()
完成,就会进入下一轮循环。
下面是这个自动执行器的一个使用实例。
async function f() {
async function* gen() {
yield 'a';
yield 'b';
yield 'c';
}
return await takeAsync(gen());
}
f().then(function (result) {
console.log(result); // ['a', 'b', 'c']
})
复制代码
异步 Generator 函数出现之后,JavaScript就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不一样之处。
最后,同步的数据结构,也可使用异步 Generator 函数。
async function* createAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}
复制代码
上面代码中,因为没有异步操做,因此也就没有使用await
关键字。
yield*
语句也能够跟一个异步遍历器。
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
const result = yield* gen1();
}
复制代码
上面代码中,gen2
函数里面的result
变量,最后的值是2
。
与同步Generator函数同样,for await...of
循环会展开yield*
。
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b
复制代码