Promise
狀態
pending
: 初始狀態, 非 fulfilled
或 rejected
.
fulfilled
: 成功的操作.
rejected
: 失敗的操作.
基本用法
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
});
複製程式碼
then()
它的作用是為 Promise 例項新增狀態改變時的回撥函式。前面說過,then方法的第一個引數是resolved狀態的回撥函式,第二個引數(可選)是rejected狀態的回撥函式。
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方法。
catch()
Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
console.log('發生錯誤!', error);
});
複製程式碼
finally()
finally方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
複製程式碼
all()
Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.all([p1, p2, p3]);
上面程式碼中,Promise.all方法接受一個陣列作為引數,p1、p2、p3都是 Promise 例項,如果不是,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。(Promise.all方法的引數可以不是陣列,但必須具有 Iterator 介面,且返回的每個成員都是 Promise 例項。)
p的狀態由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。
(2)只要p1、p2、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){
// ...
});
複製程式碼
race()
const p = Promise.race([p1, p2, p3]);
複製程式碼
上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。
resolve()
有時需要將現有物件轉為 Promise 物件,Promise.resolve方法就起到這個作用。
Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
複製程式碼
reject()
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!
});
複製程式碼
我們在這裡能做什麼呢?有三種事可以做:
- 返回另一個promise;
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
複製程式碼
- 返回一個同步值(或者undefined)
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);
});
複製程式碼
promise陣列依次執行
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)
}
}
}
複製程式碼
promise常見面試題
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改變狀態之後會呼叫回撥函式,根據狀態的不同選擇需要執行的回撥函式。
async await
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
函式的語法規則總體上比較簡單,難點是錯誤處理機制。
返回 Promise 物件
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: 出錯了
複製程式碼
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(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
複製程式碼
上面程式碼中,函式getTitle
內部有三個操作:抓取網頁、取出文字、匹配頁面標題。只有這三個操作全部完成,才會執行then
方法裡面的console.log
。
await 命令
正常情況下,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 函式的實現原理
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 await...of
前面介紹過,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 函式的作用,是返回一個非同步遍歷器物件。
在語法上,非同步 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* 語句
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
複製程式碼