JS async/await
async/await: 最簡潔地寫序列的非同步操
我們知道JS是單執行緒的,IO操作都被設計為非同步的(非阻塞的),對於非同步操作我們必需要寫callback function來處理非同步操作的返回結果。
當要寫n個序列的IO操作(上一個IO結束後才繼續下一個)時, 不得不寫n個回撥函式:
get('/car/1', car =>{ get(`/seats/${car.seat}`, seat => { get(`/colors/${seat.color}`, color => { //...more_IOs }) }) });
n個序列的IO操作,以上程式碼(都寫成匿名回撥函式的話)會出現n層巢狀, 可以採用Promise 改進了程式碼巢狀(callback hell)的問題:
promiseGet('/car/1') .then(car => promiseGet(`/seats/${car.seat}`)) .then(seat => promiseGet(`/colors/${seat.color}`)) .then(color => {/*return some promise*/}) .then(...)
Promise加回調函式似乎已經足夠簡潔了,因為es6裡arrow function使得寫一個匿名函式變得很簡潔。
然而如果使用 es7 async/await, 我們連回調函式都不用寫了:
// promise version: let v1, v2; promise1() .then(v => {v1=v; return promise2(v1)}) .then(v => {v2=v; return promise3(v1,v2)}) .then(v3 => { /*..v1,v2,v3...*/ }); // async/await version: async function asyn_run(){ let v1 = await promise1(); let v2 = await promise2(v1); let v3 = await promise3(v1, v2); /*... v1, v2, v3 ... */ } async_run();//actually non-blocking
async/await只是語法糖
注意async function依然是非阻塞的非同步執行,async/await只是語法糖,也就是相當於編譯器替我們寫了回撥函式!
理解 async/await 背後的 Generator生成函式
async/await 其實被編譯成了 generator函式(function*(){...})):
async function fn(args){ let v1 = await promise1(); let v2 = await promise2(v1); ... } // 等同於 function fn(args){ return spawn(function*() { // .... 所有await替換成yield let v1 = yield promise1(); let v2 = yield promise2(v1); ... }); }
es6 提供了Generator 生成函式:JavaScript/Reference/Statements/function*" target="_blank" rel="nofollow,noindex">https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
function *gen(){ let i=0, v; while(i<2) v=yield i++; return i; } let g = gen(); g.next();//{value:0, done:false} g.next();//{value:1, done:false} g.next();//{value:2, done:true}
編譯器會把generator function拆成多個斷點(用switch/case,不同斷點執行不同case語句),並把當前斷點位置存在物件g上以便下次繼續:
//上文function *gen(){..}程式碼被babel編譯成了帶switch/case普通函式: var _marked = /*#__PURE__*/regeneratorRuntime.mark(gen); function gen() { var i, v; return regeneratorRuntime.wrap(function gen$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: i = 0, v = void 0; case 1: if (!(i < 2)) { _context.next = 7; break; } _context.next = 4; return i++; case 4: v = _context.sent; _context.next = 1; break; case 7: return _context.abrupt("return", i); case 8: case "end": return _context.stop(); } } }, _marked, this); } var g = gen(); g.next(); //{value:0, done:false} g.next(); //{value:1, done:false} g.next(); //{value:2, done:true}
-
對比: 函式與生成函式
- 函式的呼叫方式: func(): 從頭執行到return -> 返回給caller
-
生成函式的呼叫方式: 多次g.next(init_value): 每次從上一次的yield位置(同時將引數init_value傳入此位置)執行到下一個yield -> 返回給caller
- yield 相當於一個斷點,跳轉到caller, 當caller再執行g.next()時再從此斷點繼續
思考題:如何用Generator實現 async/await
前面提到async/await 其實被編譯成了 generator函式(function*(){...})):
async function fn(args){ let v1 = await promise1(); let v2 = await promise2(v1); ... } // 等同於 function fn(args){ return spawn(function*() { // .... 所有await替換成yield let v1 = yield promise1(); let v2 = yield promise2(v1); ... }); }
思考下如何寫出spawn函式?
function spawn(generator){ let g = generator();// create a generator(object) to run return new Promise((final_resolve, final_reject) => { // inner closure function with final_resolve/final_reject function feed_and_take_one(last_async_value, isError) { let this_yield = isError ? g.throw(last_async_value) : g.next(last_async_value);//feed last_async_value back to last yield point let promise = this_yield.value; if (this_yield.done) final_resolve(promise); else promise.then(feed_and_take_one, e=>feed_and_take_one(e, true)) } feed_and_take_one(); }); }
思路:
- caller建立一個g=gen()物件,執行g.next() 得到一個yield出來的promise
-
promise中得到結果resolve_value後, 再次執行g.next(resolve_value)傳回生成函式,等待返回下一個promise
- 也就是在promise的回撥中遞迴呼叫步驟2
- 步驟2中yield value為done狀態時,停止(遞迴)
測試spawn函式:
// manual testing: function pause_and_return(n){ return new Promise(resolve =>{ console.log(`waiting ${n} seconds...`); setTimeout(()=>resolve(n), n*1000); }); } function throw_async_error(msg){ return new Promise((_, reject)=> reject(new Error(msg))); } // async/await version: async function async_routine(){ try { for (let i of [3, 2, 1]) { let a = await pause_and_return(i); console.log(`-----yield async_value: ${a}`); } await throw_async_error("my error"); await pause_and_return(100);//shouldn't run this after throw }catch(e){ console.log(`------catch error: ${e}`); return pause_and_return(6); } } async_routine().then(v => console.log(v)); console.log("====== non blocking outside async"); // spawn version: async by generator function *coroutine(){ try { for (let i of [3, 2, 1]) { let a = yield pause_and_return(i); console.log(`-----yield async_value: ${a}`); } yield throw_async_error("my error"); yield pause_and_return(100);//shouldn't run this after throw }catch(e){ console.log(`------catch error: ${e}`); return pause_and_return(6); } } spawn(coroutine).then(v => console.log(`return final async value: ${v}`));