javascript
async js 返回值_JS异步编程 | Async / Await / Generator 实现原理解析
async/await實現
在多個回調依賴的場景中,盡管Promise通過鏈式調用取代了回調嵌套,但過多的鏈式調用可讀性仍然不佳,流程控制也不方便,ES7 提出的async 函數,終于讓 JS 對于異步操作有了終極解決方案,簡潔優美地解決了以上兩個問題。
設想一個這樣的場景,異步任務a->b->c之間存在依賴關系,如果我們通過then鏈式調用來處理這些關系,可讀性并不是很好。
如果我們想控制其中某個過程,比如在某些條件下,b不往下執行到c,那么也不是很方便控制。
Promise.resolve(a) .then(b => { // do something }) .then(c => { // do something })但是如果通過async/await來實現這個場景,可讀性和流程控制都會方便不少。
async () => { const a = await Promise.resolve(a); const b = await Promise.resolve(b); const c = await Promise.resolve(c);}那么我們要如何實現一個async/await呢,首先我們要知道,async/await實際上是對Generator(生成器)的封裝,是一個語法糖。
由于Generator出現不久就被async/await取代了,很多同學對Generator比較陌生,因此我們先來看看Generator的用法:
ES6 新引入了 Generator 函數,可以通過 yield 關鍵字,把函數的執行流掛起,通過next()方法可以切換到下一個狀態,為改變執行流程提供了可能,從而為異步編程提供解決方案。
function* myGenerator() { yield '1' yield '2' return '3'}const gen = myGenerator(); // 獲取迭代器gen.next() //{value: "1", done: false}gen.next() //{value: "2", done: false}gen.next() //{value: "3", done: true}也可以通過給next()傳參, 讓yield具有返回值
function* myGenerator() { console.log(yield '1') //test1 console.log(yield '2') //test2 console.log(yield '3') //test3}// 獲取迭代器const gen = myGenerator();gen.next()gen.next('test1')gen.next('test2')gen.next('test3')我們看到Generator的用法,應該?會感到很熟悉,*/yield和async/await看起來其實已經很相似了,它們都提供了暫停執行的功能,但二者又有三點不同:
- async/await自帶執行器,不需要手動調用next()就能自動執行下一步
- async函數返回值是Promise對象,而Generator返回的是生成器對象
- await能夠返回Promise的resolve/reject的值
我們對async/await的實現,其實也就是對應以上三點封裝Generator。
自動執行
我們先來看一下,對于這樣一個Generator,手動執行是怎樣一個流程。
function* myGenerator() { yield Promise.resolve(1); yield Promise.resolve(2); yield Promise.resolve(3);}// 手動執行迭代器const gen = myGenerator()gen.next().value.then(val => { console.log(val) gen.next().value.then(val => { console.log(val) gen.next().value.then(val => { console.log(val) }) })})//輸出1 2 3我們也可以通過給gen.next()傳值的方式,讓yield能返回resolve的值。
function* myGenerator() { console.log(yield Promise.resolve(1)) //1 console.log(yield Promise.resolve(2)) //2 console.log(yield Promise.resolve(3)) //3}// 手動執行迭代器const gen = myGenerator()gen.next().value.then(val => { // console.log(val) gen.next(val).value.then(val => { // console.log(val) gen.next(val).value.then(val => { // console.log(val) gen.next(val) }) })})顯然,手動執行的寫法看起來既笨拙又丑陋,我們希望生成器函數能自動往下執行,且yield能返回resolve的值。
基于這兩個需求,我們進行一個基本的封裝,這里async/await是關鍵字,不能重寫,我們用函數來模擬:
function run(gen) { var g = gen() //由于每次gen()獲取到的都是最新的迭代器,因此獲取迭代器操作要放在_next()之前,否則會進入死循環 function _next(val) { //封裝一個方法, 遞歸執行g.next() var res = g.next(val) //獲取迭代器對象,并返回resolve的值 if(res.done) return res.value //遞歸終止條件 res.value.then(val => { //Promise的then方法是實現自動迭代的前提 _next(val) //等待Promise完成就自動執行下一個next,并傳入resolve的值 }) } _next() //第一次執行}對于我們之前的例子,我們就能這樣執行:
function* myGenerator() { console.log(yield Promise.resolve(1)) //1 console.log(yield Promise.resolve(2)) //2 console.log(yield Promise.resolve(3)) //3}run(myGenerator)這樣我們就初步實現了一個async/await。
上邊的代碼只有五六行,但并不是一下就能看明白的,我們之前用了四個例子來做鋪墊,也是為了讓讀者更好地理解這段代碼。
簡單來說,我們封裝了一個run方法,run方法里我們把執行下一步的操作封裝成_next(),每次Promise.then()的時候都去執行_next(),實現自動迭代的效果。
在迭代的過程中,我們還把resolve的值傳入gen.next(),使得yield得以返回Promise的resolve的值
這里插一句,是不是只有.then方法這樣的形式才能完成我們自動執行的功能呢?答案是否定的,yield后邊除了接Promise,還可以接thunk函數,thunk函數不是一個新東西,所謂thunk函數,就是單參的只接受回調的函數。
無論是Promise還是thunk函數,其核心都是通過傳入回調的方式來實現Generator的自動執行。thunk函數只作為一個拓展知識,理解有困難的同學也可以跳過這里,并不影響后續理解。
返回Promise & 異常處理
雖然我們實現了Generator的自動執行以及讓yield返回resolve的值,但上邊的代碼還存在著幾點問題:
- 需要兼容基本類型:這段代碼能自動執行的前提是yield后面跟Promise,為了兼容后面跟著基本類型值的情況,我們需要把yield跟的內容(gen().next.value)都用Promise.resolve()轉化一遍
- 缺少錯誤處理:上邊代碼里的Promise如果執行失敗,就會導致后續執行直接中斷,我們需要通過調用Generator.prototype.throw(),把錯誤拋出來,才能被外層的try-catch捕獲到
- 返回值是Promise:async/await的返回值是一個Promise,我們這里也需要保持一致,給返回值包一個Promise
我們改造一下run方法:
function run(gen) { //把返回值包裝成promise return new Promise((resolve, reject) => { var g = gen() function _next(val) { //錯誤處理 try { var res = g.next(val) } catch(err) { return reject(err); } if(res.done) { return resolve(res.value); } //res.value包裝為promise,以兼容yield后面跟基本類型的情況 Promise.resolve(res.value).then( val => { _next(val); }, err => { //拋出錯誤 g.throw(err) }); } _next(); });}然后我們可以測試一下:
function* myGenerator() { try { console.log(yield Promise.resolve(1)) console.log(yield 2) //2 console.log(yield Promise.reject('error')) } catch (error) { console.log(error) }}const result = run(myGenerator) //result是一個Promise//輸出 1 2 error到這里,一個async/await的實現基本完成了。最后我們可以看一下babel對async/await的轉換結果,其實整體的思路是一樣的,但是寫法稍有不同:
//相當于我們的run()function _asyncToGenerator(fn) { // return一個function,和async保持一致。我們的run直接執行了Generator,其實是不太規范的 return function() { var self = this var args = arguments return new Promise(function(resolve, reject) { var gen = fn.apply(self, args); //相當于我們的_next() function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value); } //處理異常 function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err); } _next(undefined); }); };}function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); }}使用方式:
const foo = _asyncToGenerator(function* () { try { console.log(yield Promise.resolve(1)) //1 console.log(yield 2) //2 return '3' } catch (error) { console.log(error) }})foo().then(res => { console.log(res) //3})有關async/await的實現,到這里就告一段落了。但是直到結尾,我們也不知道await到底是如何暫停執行的,有關await暫停執行的秘密,我們還要到Generator的實現中去尋找答案。
Generator實現
我們從一個簡單的Generator使用實例開始,一步步探究Generator的實現原理:
function* foo() { yield 'result1' yield 'result2' yield 'result3'} const gen = foo()console.log(gen.next().value)console.log(gen.next().value)console.log(gen.next().value)我們可以在babel官網上在線轉化這段代碼,看看ES5環境下是如何實現Generator的:
"use strict";var _marked =/*#__PURE__*/regeneratorRuntime.mark(foo);function foo() { return regeneratorRuntime.wrap(function foo$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return 'result1'; case 2: _context.next = 4; return 'result2'; case 4: _context.next = 6; return 'result3'; case 6: case "end": return _context.stop(); } } }, _marked);}var gen = foo();console.log(gen.next().value);console.log(gen.next().value);console.log(gen.next().value);代碼咋一看不長,但如果仔細觀察會發現有兩個不認識的東西 —— regeneratorRuntime.mark和regeneratorRuntime.wrap,這兩者其實是 regenerator-runtime 模塊里的兩個方法。
regenerator-runtime 模塊來自facebook的 regenerator 模塊,完整代碼在runtime.js,這個runtime有700多行...-_-||,因此我們不能全講,不太重要的部分我們就簡單地過一下,重點講解暫停執行相關部分代碼。
個人覺得啃源碼的效果不是很好,建議讀者拉到末尾先看結論和簡略版實現,源碼作為一個補充理解。
regeneratorRuntime.mark()
regeneratorRuntime.mark(foo)這個方法在第一行被調用,我們先看一下runtime里mark()方法的定義。
//runtime.js里的定義稍有不同,多了一些判斷,以下是編譯后的代碼runtime.mark = function(genFun) { genFun.__proto__ = GeneratorFunctionPrototype; genFun.prototype = Object.create(Gp); return genFun;};這里邊GeneratorFunctionPrototype和Gp我們都不認識,他們被定義在runtime里,不過沒關系,我們只要知道mark()方法為生成器函數(foo)綁定了一系列原型就可以了,這里就簡單地過了。
regeneratorRuntime.wrap()
從上面babel轉化的代碼我們能看到,執行foo(),其實就是執行wrap(),那么這個方法起到什么作用呢,他想包裝一個什么東西呢,我們先來看看wrap方法的定義:
//runtime.js里的定義稍有不同,多了一些判斷,以下是編譯后的代碼function wrap(innerFn, outerFn, self) { var generator = Object.create(outerFn.prototype); var context = new Context([]); generator._invoke = makeInvokeMethod(innerFn, self, context); return generator;}wrap方法先是創建了一個generator,并繼承outerFn.prototype;然后new了一個context對象;makeInvokeMethod方法接收innerFn(對應foo$)、context和this,并把返回值掛到generator._invoke上;最后return了generator。
其實wrap()相當于是給generator增加了一個_invoke方法。
這段代碼肯定讓人產生很多疑問,outerFn.prototype是什么,Context又是什么,makeInvokeMethod又做了哪些操作。下面我們就來一一解答:
outerFn.prototype其實就是genFun.prototype
這個我們結合一下上面的代碼就能知道
context可以直接理解為這樣一個全局對象,用于儲存各種狀態和上下文:
var ContinueSentinel = {};var context = { done: false, method: "next", next: 0, prev: 0, abrupt: function(type, arg) { var record = {}; record.type = type; record.arg = arg; return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === "return") { this.rval = this.arg = record.arg; this.method = "return"; this.next = "end"; } return ContinueSentinel; }, stop: function() { this.done = true; return this.rval; }};makeInvokeMethod的定義如下,它return了一個invoke方法,invoke用于判斷當前狀態和執行下一步,其實就是我們調用的next()
//以下是編譯后的代碼function makeInvokeMethod(innerFn, context) { // 將狀態置為start var state = "start"; return function invoke(method, arg) { // 已完成 if (state === "completed") { return { value: undefined, done: true }; } context.method = method; context.arg = arg; // 執行中 while (true) { state = "executing"; var record = { type: "normal", arg: innerFn.call(self, context) // 執行下一步,并獲取狀態(其實就是switch里邊return的值) }; if (record.type === "normal") { // 判斷是否已經執行完成 state = context.done ? "completed" : "yield"; // ContinueSentinel其實是一個空對象,record.arg === {}則跳過return進入下一個循環 // 什么時候record.arg會為空對象呢, 答案是沒有后續yield語句或已經return的時候,也就是switch返回了空值的情況(跟著上面的switch走一下就知道了) if (record.arg === ContinueSentinel) { continue; } // next()的返回值 return { value: record.arg, done: context.done }; } } };}為什么generator._invoke實際上就是gen.next呢,因為在runtime對于next()的定義中,next()其實就return了_invoke方法
// Helper for defining the .next, .throw, and .return methods of the// Iterator interface in terms of a single ._invoke method.function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; });}defineIteratorMethods(Gp);低配實現 & 調用流程分析
這么一遍源碼下來,估計很多讀者還是懵逼的,畢竟源碼中糾集了很多概念和封裝,一時半會不好完全理解,讓我們跳出源碼,實現一個簡單的Generator,然后再回過頭看源碼,會得到更清晰的認識。
// 生成器函數根據yield語句將代碼分割為switch-case塊,后續通過切換_context.prev和_context.next來分別執行各個casefunction gen$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return 'result1'; case 2: _context.next = 4; return 'result2'; case 4: _context.next = 6; return 'result3'; case 6: case "end": return _context.stop(); } }}// 低配版context var context = { next:0, prev: 0, done: false, stop: function stop () { this.done = true }}// 低配版invokelet gen = function() { return { next: function() { value = context.done ? undefined: gen$(context) done = context.done return { value, done } } }} // 測試使用var g = gen() g.next() // {value: "result1", done: false}g.next() // {value: "result2", done: false}g.next() // {value: "result3", done: false}g.next() // {value: undefined, done: true}這段代碼并不難理解,我們分析一下調用流程:
- 我們定義的function*生成器函數被轉化為以上代碼
- 轉化后的代碼分為三大塊:
- 當我們調用g.next(),就相當于調用invoke()方法,執行gen$(_context),進入switch語句,switch根據context的標識,執行對應的case塊,return對應結果
- 當生成器函數運行到末尾(沒有下一個yield或已經return),switch匹配不到對應代碼塊,就會return空值,這時g.next()返回{value: undefined, done: true}
從中我們可以看出,Generator實現的核心在于上下文的保存,函數并沒有真的被掛起,每一次yield,其實都執行了一遍傳入的生成器函數,只是在這個過程中間用了一個context對象儲存上下文,使得每次執行生成器函數的時候,都可以從上一個執行結果開始執行,看起來就像函數被掛起了一樣。
總結
以上是生活随笔為你收集整理的async js 返回值_JS异步编程 | Async / Await / Generator 实现原理解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信如何开通零钱通
- 下一篇: 信用卡跳码是什么意思