Async / Await / Generator 實現原理

async/await實現

在多個回調依賴的場景中,儘管Promise經過鏈式調用取代了回調嵌套,但過多的鏈式調用可讀性仍然不佳,流程控制也不方便,ES7 提出的async 函數,終於讓 JS 對於異步操做有了終極解決方案,簡潔優美地解決了以上兩個問題。javascript

設想一個這樣的場景,異步任務a->b->c之間存在依賴關係,若是咱們經過then鏈式調用來處理這些關係,可讀性並非很好。java

若是咱們想控制其中某個過程,好比在某些條件下,b不往下執行到c,那麼也不是很方便控制。編程

Promise.resolve(a)
  .then(b => {
    // do something
  })
  .then(c => {
    // do something
  })

可是若是經過async/await來實現這個場景,可讀性和流程控制都會方便很多。promise

async () => {
  const a = await Promise.resolve(a);
  const b = await Promise.resolve(b);
  const c = await Promise.resolve(c);
}

那麼咱們要如何實現一個async/await呢,首先咱們要知道,async/await其實是對Generator(生成器)的封裝,是一個語法糖。babel

因爲Generator出現不久就被async/await取代了,不少同窗對Generator比較陌生,所以咱們先來看看Generator的用法:app

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具備返回值async

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來分別執行各個case
function 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
  }
}

// 低配版invoke
let 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*生成器函數被轉化爲以上代碼
  • 轉化後的代碼分爲三大塊:
  1. gen$(_context)由yield分割生成器函數代碼而來
  2. context對象用於儲存函數執行上下文
  3. invoke()方法定義next(),用於執行gen$(_context)來跳到下一步
  • 當咱們調用g.next(),就至關於調用invoke()方法,執行gen$(_context),進入switch語句,switch根據context的標識,執行對應的case塊,return對應結果
  • 當生成器函數運行到末尾(沒有下一個yield或已經return),switch匹配不到對應代碼塊,就會return空值,這時g.next()返回{value: undefined, done: true}

從中咱們能夠看出,Generator實現的核心在於上下文的保存,函數並無真的被掛起,每一次yield,其實都執行了一遍傳入的生成器函數,只是在這個過程當中間用了一個context對象儲存上下文,使得每次執行生成器函數的時候,均可以從上一個執行結果開始執行,看起來就像函數被掛起了同樣。

相關文章
相關標籤/搜索