從Generator到Async function
寫在前面
說到非同步函式,不由地想起Wind.js,以及老趙的遠見:
Wind.js在JavaScript非同步程式設計領域絕對是一個創新,可謂前無來者。有朋友就評價說“在看到Wind.js之前,真以為這是不可能實現的”,因為Wind.js事實上是用類庫的形式“修補”了JavaScript語言,也正是這個原因,才能讓JavaScript非同步程式設計體驗獲得質的飛躍。 ——2012年7月 ES2017的async&await從promise,generator一路輾轉走來,而Wind早在6年前就看到了這一天,並提前實現了願景
一.yield與await
為什麼說Async function是從Promise,Generator一路走來的?
因為非同步函式與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}
二.暫停呢?
生成器能讓執行流“喘口氣”,能讓停不下來的東西暫停,能用來重構迴圈,能駕馭無限序列,能包裝迭代器。。。好處多多
(摘自ofollow,noindex" target="_blank">generator(生成器)_ES6筆記2 )
但上例中好像並沒有看到暫停的效果,我們加點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
那麼,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)
五.淵源
至此,我們已經用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,都戳在紙帶上,塞進計算機,取出執行結果)咦,這……尼瑪,咋還出錯了捏?