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
複製代碼