寫在前面
說到異步函數,不由地想起 [Wind.js](/articles/向 windjs 致敬-node 異步流程控制 4/),以及老趙的遠見:
Wind.js 在 JavaScript 異步編程領域絕對是一個創新,可謂前無來者。有朋友就評價說「在看到 Wind.js 之前,真以為這是不可能實現的」,因為 Wind.js 事實上是用類庫的形式「修補」了 JavaScript 語言,也正是這個原因,才能讓 JavaScript 異步編程體驗獲得質的飛躍。 ——2012 年 7 月
ES2017 的 async&await 從 promise,generator 一路輾轉走來,而 Wind 早在 6 年前就看到了這一天,並提前實現了願景
一。yield 與 await
為什麼說 Async function 是從 [Promise](/articles/完全理解 promise/),[Generator](/articles/generator(生成器)-es6 筆記 2/) 一路走來的?
因為異步函數與 Generator 特性有著千絲萬縷的關係,比如,語義上都有暫停的意思:
-
yield:讓步,歇會兒喘口氣 -
await:橋多麻袋
先對比一個最簡單的場景:
// generator
function* gen() {
console.log('Do step 1');
yield 'Until step1 completed';
console.log('Do step 2');
}
let iter = gen();
iter.next();
iter.next();
// async function
async function f() {
console.log('Do step 1');
await 'Until step1 completed';
console.log('Do step 2');
}
f();
二者代碼結構相似,並且輸出也類似(作為兩個例子分開執行):
// generator
Do step 1
Do step 2
{value: undefined, done: true}
// async function
Do step 1
Do step 2
Promise?{<resolved>: undefined}
二。暫停呢?
生成器能讓執行流「喘口氣」,能讓停不下來的東西暫停,能用來重構循環,能駕馭無限序列,能包裝迭代器。。。好處多多
(摘自 [generator(生成器)_ES6 筆記 2](/articles/generator(生成器)-es6 筆記 2/#articleHeader9))
但上例中好像並沒有看到暫停的效果,我們加點 log,讓一切更明顯一些:
// generator
function* gen() {
console.log('Do step 1');
yield 'Until step1 completed';
console.log('Do step 2');
}
let iter = gen();
iter.next();
console.log('generator 抽根兒煙');
iter.next();
// async function
async function f() {
console.log('Do step 1');
await 'Until step1 completed';
console.log('Do step 2');
}
f();
console.log('async function 抽根兒煙');
這次不關注各自的返回值(上面已經看過了),連在一起執行,輸出結果如下:
Do step 1
generator 抽根兒煙
Do step 2
Do step 1
async function 抽根兒煙
Do step 2
輸出沒什麼差異,但 log('xxx 抽根兒煙') 所在的位置有差異
實際區別在於,上例中 Generator 的執行過程是純同步的,而 async function 的執行過程含有異步的部分,用 Generator 來描述的話,相當於:
// generator 假裝 async function
function* gen() {
console.log('Do step 1');
yield Promise.resolve('Until step1 completed');
console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator 假裝 async function 抽根兒煙');
// 輸出結果
Do step 1
generator 假裝 async function 抽根兒煙
Do step 2
三。近一點,更近一點
更進一步地,很容易用 Generator 去實現 Async function 特性:
function asyncFunction(gen, ...args) {
return new Promise((resolve, reject) => {
resolve(safeNext(gen(...args)));
});
}
function safeNext(iter, last) {
let step;
try {
step = iter.next(last);
} catch(ex) {
step = iter.throw(ex);
}
return Promise.resolve(step.value)
.catch(ex => iter.throw(ex).value)
.then(result => step.done ? result : safeNext(iter, result))
}
P.S.Github repo 地址 ayqy/asyncFunction
試玩一下:
asyncFunction(function* (){
console.log('Do step 1');
// Wait 100ms
let x = yield new Promise((resolve, reject) => {
setTimeout(resolve.bind(null, 1), 100);
});
// 100ms later
console.log(`Step1 completed, got ${x}`);
try {
throw ++x;
} catch(ex) {
x = -1;
}
console.log(`x = ${x}`);
x = yield x * 2;
console.log(`All steps passed, got ${x}`);
return x;
}).then(result => {
console.log(`Final result ${result}`);
});
let intervalId = setInterval(console.log.bind(console, 'tick'), 10);
setTimeout(() => {
clearInterval(intervalId);
}, 100);
會得到類似輸出:
Do step 1
3
⑨tick
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
tick
其中第二行的 3 是 setTimeout 返回值(因此 asyncFunction 中只有第一段是同步執行的),第三行輸出 9 次 'tick' 表示過了 90 多 ms,此時 Wait 100ms 結束了,接著執行剩餘部分直到結束
另外,還有一個難以察覺的細節是,本例中剩餘部分的執行不會被 interval 回調打斷(即便間隔極短),例如:
asyncFunction(function* (){
setTimeout(console.log.bind(console, '#0'), 0)
console.log('Do step 1');
// Wait 100ms
let x = yield new Promise((resolve, reject) => {
setTimeout(resolve.bind(null, 1), 100);
});
setTimeout(console.log.bind(console, '#1'), 0)
// 100ms later
console.log(`Step1 completed, got ${x}`);
setTimeout(console.log.bind(console, '#2'), 0)
try {
throw ++x;
} catch(ex) {
x = -1;
}
setTimeout(console.log.bind(console, '#3'), 0)
console.log(`x = ${x}`);
x = yield x * 2;
setTimeout(console.log.bind(console, '#4'), 0)
console.log(`All steps passed, got ${x}`);
return x;
}).then(result => {
console.log(`Final result ${result}`);
});
輸出結果是:
Do step 1
Promise?{<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4
#1, 2, 3, 4 最後輸出,這與任務類型有關,具體見 macrotask 與 microtask
對比正版 async function:
(async function(){
setTimeout(console.log.bind(console, '#0'), 0)
console.log('Do step 1');
// Wait 100ms
let x = await new Promise((resolve, reject) => {
setTimeout(resolve.bind(null, 1), 100);
});
setTimeout(console.log.bind(console, '#1'), 0)
// 100ms later
console.log(`Step1 completed, got ${x}`);
setTimeout(console.log.bind(console, '#2'), 0)
try {
throw ++x;
} catch(ex) {
x = -1;
}
setTimeout(console.log.bind(console, '#3'), 0)
console.log(`x = ${x}`);
x = await x * 2;
setTimeout(console.log.bind(console, '#4'), 0)
console.log(`All steps passed, got ${x}`);
return x;
})().then(result => {
console.log(`Final result ${result}`);
});
輸出完全一致:
Do step 1
Promise?{<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4
四。語法糖?
基本語法形式如下:
async function name([param[, param[, ... param]]]) {
statements
}
需要知道 2 點:
-
await關鍵字只能出現在 Async function 裡,否則報錯 -
Async function 的返回值是 Promise
實際上,async function 共有 4 種形式:
-
函數聲明:
async function foo() {} -
函數表達式:
const foo = async function () {}; -
方法定義:
let obj = { async foo() {} } -
箭頭函數:
const foo = async () => {};
例如:
async function fetchJson(url) {
try {
console.log('Starting fetch');
let request = await fetch(url);
let text = await request.text();
return JSON.parse(text);
} catch(error) {
console.error(error);
}
}
// test
fetchJson('https://unpkg.com/emoutils/package.json')
.then(json => console.log(json));
console.log('Fetching...');
輸出:
Starting fetch
Fetching...
undefined
{name: "emoutils",?…}
咦,異步函數貌似並不「異步」,Async function 函數體的第一段(第一個 await 之前的部分)是同步執行的,類似於:
new Promise(resolve => {
console.log('Starting fetch');
setTimeout(resolve.bind(null, 'data'), 100);
}).then(data => console.log(data));
console.log('Fetching...');
同樣,很容易把這個東西換成我們的盜版:
asyncFunction(function* fetchJson(url) {
try {
console.log('Starting fetch');
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
} catch(error) {
console.error(error);
}
}, 'https://unpkg.com/emoutils/package.json')
.then(json => console.log(json));
// test
console.log('Fetching...');
事實上我們做了 3 件事:
-
把函數體用 Generator 包起來,
await都換成yield -
去掉
async與function之間的空格並駝峰命名 -
把參數挪到 Generator 後面去
如果把這 3 件事通過編譯轉換屏蔽掉的話(甚至簡單匹配替換就能做到):
function afunction(templateData) {
const source = templateData;
// ...一頓操作把上面字符串內容轉換成
let params = ['url'];
let transformed = `function* fetchJson(url) {
try {
console.log('Starting fetch');
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
} catch(error) {
console.error(error);
}
}`;
return function(...args) {
return asyncFunction(new Function(...params, `return ${transformed}`)(), ...args);
};
}
async function 特性就被盜版方案完全取代了,語法形式也可以變得更相近:
afunction`(url) => {
try {
console.log('Starting fetch');
let request = await fetch(url);
let text = await request.text();
return JSON.parse(text);
} catch(error) {
console.error(error);
}
}`('https://unpkg.com/emoutils/package.json')
.then(json => console.log(json));
P.S. 這裡應用了 ES2015 標籤模板(tagged templates)特性,具體見 [模板字符串_ES6 筆記 3](/articles/模板字符串-es6 筆記 3/#articleHeader6)
那麼,Async function 是語法糖嗎?
可以認為是。因為有了 Generator 特性後,Async function 也就呼之欲出了(從 yield 到 await,本質上只是進一步提升了異步編程體驗,算是微改進):
Internally, async functions work much like generators, but they are not translated to generator functions.
但語言層面的特性支持要比類似編譯轉換的替代方案更具優勢,體現在性能、錯誤追蹤(乾淨的調用棧)、與其它特性無縫貼合(如箭頭函數、方法定義)等方面
異步編程體驗
從編程體驗上來看,Async function 特���帶來的提升在於:
-
以同步形式編寫異步代碼,異步、回調等概念被淡化了
-
try-catch 能夠捕獲到異步操作中的異常
能讓含有異步操作的代碼塊仍然順序執行,這無疑是最好的異步編程體驗了:
// callback reqXXX(參數,成功回調,失敗回調)
reqLogin(password, reqOrderNo, notFound);
reqOrderNo(uid, reqOrderDetail, notFound);
reqOrderDetail(orderNo, render, boom);
render(data);
// promise
promisifiedReqLogin(password)
.then(({ uid }) => promisifiedReqOrderNo(uid), notFound)
.then(({ orderNo }) => promisifiedReqOrderDetail(orderNo), notFound)
.then(({ data }) => render(data))
.catch(boom)
// async function
async function renderPage(password) {
let uid, orderNo;
try {
uid = await promisifiedReqLogin(password);
orderNo = await promisifiedReqOrderNo(uid);
} catch(ex) {
notFound(ex);
}
let data = await promisifiedReqOrderDetail(orderNo);
return render(data);
}
renderPage().catch(boom);
data = await fetchData(),僅此而已。回調的概念不復存在,減輕了大腦跟著異步操作入棧出棧的負擔,畢竟
代碼是寫給人看的,附帶可以在機器上運行
(摘自 [寫好 JavaScript](/articles/寫好 javascript/))
五。淵源
至此,我們已經用 Generator 和 Promise 特性實現了盜版 Async function,甚至沒費多少工夫(僅 18 行代碼)
現在回想一下我們是如何把這兩個特性組合起來的?或者說,依靠這兩個特性的哪些機制,讓盜版得以輕鬆實現?
首先,要實現 Async function 的話,最关键的特性是 Generator,通過 yield 讓順序執行流停下來,才有「等待」一說:
function* infSeq() {
let i = 0;
// 不會發生死循環喲,yield 讓 while true「停」下來了
while (true) {
console.log(i);
yield i++;
}
}
// test
let iter = infSeq();
// 輸出 0, 1, 2...
iter.next();
iter.next();
iter.next();
能「等待」了,那麼等誰呢?直接等異步操作嗎?如何區分異步操作?
沒錯,該 Promise 上場了:
// generator 假裝 async function
function* gen() {
console.log('Do step 1');
yield Promise.resolve('Until step1 completed');
console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator 假裝 async function 抽根兒煙');
只要 next().value 是 Promise,那就肯定是異步操作,等它完成了再 next(),這樣就實現了等待一個異步操作做完再繼續下面的事情,即 Async function 特性
從上層概念上來看,三者關係如下:
Async function = 調度機(Generator) + 異步任務(Promise)
其中,Generator 這個調度機的作用在於:
-
分片(拆不開怎麼等):將函數體順序代碼塊拆分成幾段
-
調度(拆開了怎麼執行):從外部控制這些片段的執行流,如
next()、throw()等
Promise 作為異步任務模型,主要特點如下:
-
狀態丟棄:一次性的 Promise 對象,用完即扔(
then()等都返回新 Promise) -
任務組合:可以通過類似
resolve(promise)的方式形成任務鏈,結合all()、race()等控制其順序 -
錯誤上拋:類似於冒泡的異常處理機制,沿任務鏈向上拋出異常,簡化了異步任務的異常捕獲
Generator 並不直接調度 Promise(調度的對象是被拆開的片段),但它關注每一段的執行結果,如果結果是 pending Promise,就等到不 pending 了,再控制下一段執行
所以,Promise 只是配角兒,可以替換成任意的異步任務模型,其主要作用在於告知 Generator 這裡有個異步操作得等一下:
調度機:(把一段代碼戳在紙帶上,塞進計算機,取出執行結果)咦,這是個啥?
異步任務:Hey,我是個異步任務啊,還沒完事兒,完了我告訴你
調度機:好,我抽根兒煙(頭像變灰)
異步任務:完事了完事了,結果是 xxx
調度機:(立即上線,拿起下一段代碼和 xxx,都戳在紙帶上,塞進計算機,取出執行結果)咦,這……尼瑪,咋還出錯了捏?
暫無評論,快來發表你的看法吧