面試官: 說說你對async的理解

  • 2020 年 4 月 11 日
  • 筆記

大家好,我是小雨小雨,致力於分享有趣的、實用的技術文章。

內容分為翻譯和原創,如果有問題,歡迎隨時評論或私信,希望和大家一起進步。

分享不易,希望能夠得到大家的支持和關注。

TL;DR

async是generator和promise的語法糖,利用迭代器的狀態機和promise來進行自更新!

如果懶得往下看,可以看下這個極其簡易版本的實現方式:

// 複製粘貼即可直接運行  function stateMac (arr) {      let val;      return {          next(){              if ((val = arr.shift())) {                  return {                      value: val,                      done: false                  }              } else {                  return {                      done: true                  }              }          }      }  }      function asyncFn(arr) {      const iterator = stateMac(arr);      function doSelf () {          const cur = iterator.next();          const value = cur.value;          if (cur.done) {              console.log('done');              return;          }          switch (true) {              case value.then && value.toString() === '[object Promise]':                  value.then((result) => {                      console.log(result);                      doSelf();                  })                  break;              case typeof value === 'function':                  value();                  doSelf();                  break;              default:                  console.log(value);                  doSelf();          }      }      doSelf();  }    const mockAsync = [      1,      new Promise((res) => {          setTimeout(function () {              res('promise');          }, 3000);      }),      function () {          console.log('測試');      }  ];  console.log('開始');  asyncFn(mockAsync);  console.log('結束');  

前言

async & await 和我們的日常開發緊密相連,但是你真的了解其背後的原理嗎?

本文假設你對promise、generator有一定了解。

簡述promise

promise就是callback的另一種寫法,避免了毀掉地獄,從橫向改為縱向,大大提升了可讀性和美觀。

至於promise的實現,按照promise A+規範一點點寫就好了,完成後可以使用工具進行測試,確保你的寫的東西是符合規範的。

具體實現原理,市面上有各種各樣的寫法,我就不多此一舉了。

簡述generator

generator就不像promise那樣,他改變了函數的執行方式。可以理解為協程,就是說多個函數互相配合完成任務。類似於這個東西:

function generator() {      return {          _value: [1, 2, 3, 4],          next() {              return {                  value: this._value.shift(),                  done: !this._value.length              };          }      };  }  const it = generator();    console.log(it.next());  console.log(it.next());  console.log(it.next());  console.log(it.next());  

這只是一個demo,僅供參考。

具體請參考MDN.

async & await

照我的理解,其實就是generator和promise相交的產物,被解析器識別,然後轉換成我們熟知的語法。

這次要做的就是去看編譯之後的結果是什麼樣的。

既然如此,我們就帶着問題去看,不然看起來也糟心不是~

async包裝的函數會返回一個什麼樣的promise?

// 源代碼:  async function fn() {}    fn();  
// 編譯後變成了一大坨:    // generator的polyfill  require("regenerator-runtime/runtime");    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);    }  }    function _asyncToGenerator(fn) {    return function() {      var self = this,        args = arguments;      return new Promise(function(resolve, reject) {        var gen = fn.apply(self, args);        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 fn() {    return _fn.apply(this, arguments);  }    function _fn() {    _fn = _asyncToGenerator(      /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {        return regeneratorRuntime.wrap(function _callee$(_context) {          while (1) {            switch ((_context.prev = _context.next)) {              case 0:              case "end":                return _context.stop();            }          }        }, _callee);      })    );    return _fn.apply(this, arguments);  }    fn();  

內容也不是很多,我們一點點來看:

generator包裝

fn內部調用的是_fn,一個私有方法,使用的apply綁定的this,並傳入了動態參數。

_fn內調用了_asyncToGenerator方法,由於js調用棧後進先出:

讀起來是這樣的:fn() => _asyncToGenerator => .mark()

執行是反過來的:.mark() => _asyncToGenerator => fn()

我們先往裡看,映入眼帘的是regeneratorRuntime.mark,該方法是generator的polyfill暴露的方法之一,我們去內部(require(‘regenerator-runtime/runtime’))簡單看下這個mark是用來幹什麼的。

// 立即執行函數,適配commonjs和瀏覽器  (function (exports) {      // 暴露mark方法      exports.mark = function (genFun) {          // 兼容判斷__proto__,處理老舊環境          if (Object.setPrototypeOf) {              Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);          } else {              genFun.__proto__ = GeneratorFunctionPrototype;              // 設置Symbol.toStringTag,適配toString              if (!(toStringTagSymbol in genFun)) {                  genFun[toStringTagSymbol] = 'GeneratorFunction';              }          }          // 設置原型          genFun.prototype = Object.create(Gp);          return genFun;      };  })(typeof module === 'Object' ? module.exports : {});  

mark做了兩個操作,一個是設置genFun的__proto__,一個是設置prototype,可能有人會好奇:

__proto__不是對象上的嗎?prototype不是函數上的嗎?為啥兩個同時應用到一個上面了

這樣操作是沒問題的,genFun不僅是函數啊,函數還是對象,js中萬物皆對象哦。你想想是不是可以通過Function構造函數new出一個函數?

然後開始設置__proto__和prototype,在次之前,我們來簡單捋一下原型。

原型

下面是個人理解的一個說法,未查閱v8引擎,但是這樣是說得通的。如果有問題,歡迎指出,一起溝通,我也會及時修改,以免誤導他人!!!。

首先要知道這三個的概念:搞清對象的原型對象(proto)、構造函數的原型(prototype)、構造方法(constructor)。

方便記憶,只需要記住下面幾條即可:

  • prototype是構造函數(注意:構造函數也是對象嗷)上特有的屬性,代表構造函數的原型。舉個例子:

有一位小明同學(指代構造函數),他有自己的朋友圈子(指代prototype),通過小明可以找到小紅(構造函數.prototype.小紅),在通過小紅的朋友圈子(prototype)還能找到小藍,直到有一個人(指代null),孑然一身、無欲無求,莫得朋友。

上面這個關係鏈就可以理解為原型鏈。

  • __proto__是每一個對象上特有的屬性,指向當前對象構造函數的prototype。再舉個例子:

小明家裡催的急,不就就生了個大胖小子(通過構造函數{小明}創造出對象{大胖小子}),可以說這個大胖小子一出生就被眾星捧月,小明的朋友們紛紛表示,以後孩子有啥事需要幫忙找我就成。這就指代對象上的__proto____proto__可以引用構造函數的任何關係。

所以說,代碼源於生活~

  • constructor是啥呢,就是一個prototype上的屬性,表示這個朋友圈子是誰的,對於小明來說: 小明.prototype.constructor === 小明。所以,當我們進行繼成操作的時候,有必要修正一下constructor,不然朋友圈子就亂了~

  • js中函數和對象有點套娃的意思,萬物皆對象,對象又是從構造函數構造而來。對於小明來說,就是我生我生我~~

來看兩個判斷:

proto 指向構造當前對象的構造函數的prototype,由於萬物皆對象,對象又是通過構造函數構造而來。故Object通過Function構造而來,所以指向了Function.prototype

console.log(Object.__proto__ === Function.prototype); // => true  

proto 指向構造當前對象的構造函數的prototype,由於萬物皆對象,對象又是通過構造函數構造而來。故Function通過Function構造而來,所以指向了Function.prototype

console.log(Function.__proto__ === Function.prototype); // => true  

有興趣的朋友可以再看看這篇文章


然後,我們再來看看這張圖,跟着箭頭走一遍,是不是就很清晰了?

繼續generator包裝

mark方法會指定genFun的__proto__和prototype,完完全全替換了genFun的朋友圈以及創造genFun的構造函數的朋友圈,現在genFun就是Generator的克隆品了。

用來設置__proto__ 和 prototype的值,GeneratorFunctionPrototype,GP,我們也簡單過一下:

  // 創建polyfill對象  var IteratorPrototype = {};  IteratorPrototype[iteratorSymbol] = function () {      return this;  };    // 原型相關操作  // 獲取對象的原型: __proto__  var getProto = Object.getPrototypeOf;    // 原生iterator原型  var NativeIteratorPrototype = getProto && getProto(getProto(values([])));  // IteratorPrototype設置為原生  if (      NativeIteratorPrototype &&      NativeIteratorPrototype !== Op &&      hasOwn.call(NativeIteratorPrototype, iteratorSymbol)  ) {      // This environment has a native %IteratorPrototype%; use it instead      // of the polyfill.      IteratorPrototype = NativeIteratorPrototype;  }    // 創造原型  // Gp 為 迭代器原型  // IteratorPrototype作為原型對象  var Gp = (GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(      IteratorPrototype  ));    // 更新構造函數和原型  GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;  GeneratorFunctionPrototype.constructor = GeneratorFunction;    // toString,調用Object.toString.call的時候會返回GeneratorFunction  GeneratorFunctionPrototype[      toStringTagSymbol  ] = GeneratorFunction.displayName = 'GeneratorFunction';  

最後再返回經過處理的genFun,然後再回到mark函數外~

_asyncToGenerator

_asyncToGenerator 接收mark處理過的結果:

// fn 為 generator 的克隆品  function _asyncToGenerator(fn) {      return function () {          var self = this,              args = arguments;          return new Promise(function (resolve, reject) {              // 調用_callee,先看下面,一會在回來哈~              var gen = fn.apply(self, args);              function _next(value) {                  asyncGeneratorStep(                      gen,                      resolve,                      reject,                      _next,                      _throw,                      'next',                      value                  );              }              function _throw(err) {                  asyncGeneratorStep(                      gen,                      resolve,                      reject,                      _next,                      _throw,                      'throw',                      err                  );              }              _next(undefined);          });      };  }  
regeneratorRuntime.wrap

上面的_asyncToGenerator執行後,會執行mark返回的函數:

function _callee() {      return regeneratorRuntime.wrap(function _callee$(          _context      ) {          // 這裡就是動態得了,也就是根據用戶寫的async函數,轉換的記過,由於我們是一個空函數,所以直接stop了          while (1) {              switch ((_context.prev = _context.next)) {                  case 0:                  case 'end':                      return _context.stop();              }          }      },      _callee);  }  

_callee會返回wrap處理後的結果,我們繼續看:

// innerFn是真正執行的函數,outerFn為被mark的函數  // self, tryLocsList未傳遞,為undefined  function wrap(innerFn, outerFn, self, tryLocsList) {      // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.      // outerFn 的原型已經被 mark重新設置,所以會包含generator相關原型      var protoGenerator =          outerFn && outerFn.prototype instanceof Generator              ? outerFn              : Generator;        // 創建自定義原型的對象      var generator = Object.create(protoGenerator.prototype);        // context 實例是包含的 this.tryEntries 的      var context = new Context(tryLocsList || []);        // The ._invoke method unifies the implementations of the .next,      // .throw, and .return methods.      generator._invoke = makeInvokeMethod(innerFn, self, context);        return generator;  }  

其中有個new Context()的操作,用來重置並記錄迭代器的狀態,後面會用到。
之後給返回generator掛載一個_invoke方法,調用makeInvokeMethod,並傳入self(未傳遞該參數,為undefined)和context。

function makeInvokeMethod(innerFn, self, context) {      // state只有在該函數中備操作      var state = GenStateSuspendedStart; // GenStateSuspendedStart: 'suspendedStart'        // 作為外面的返回值      return function invoke(method, arg) {          // 這裡就是generator相關的一些操作了,用到的時候再說      };  }  

利用閉包初始化state,並返回一個invoke函數,接受兩個參數,方法和值。先看到這,繼續往後看。

回到之前的_asyncToGenerator

// 返回帶有_invoke屬性的generator對象  var gen = fn.apply(self, args);  

之後定義了一個next和throw方法,隨後直接調用_next開始執行:

function _next(value) {      asyncGeneratorStep(          gen, // 迭代器函數          resolve, // promise的resolve          reject, // promise的project          _next, // 當前函數          _throw, // 下面的_throw函數          'next', // method名          value // arg 參數值      );  }  function _throw(err) {      asyncGeneratorStep(          gen,          resolve,          reject,          _next,          _throw,          'throw',          err      );  }  _next(undefined);  

其中都是用的asyncGeneratorStep,並傳遞了一些參數。

那asyncGeneratorStep又是啥呢:

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          resolve(value);      } else {          // 否則,繼續下次next調用,形成遞歸          Promise.resolve(value).then(_next, _throw);      }  }  

代碼很少,獲取即將要調用的方法名(key)並傳入參數,所以當前info即是:

var info = gen['next'](arg);  

那next是哪來的那?就是之前mark操作中定義的,如果原生支持,就是用原生的迭代器提供的next,否則使用polyfill中定義的next。

還記得之前的makeInvokeMethod嗎?

它其實是用來定義標準化next、throw和return的:

function defineIteratorMethods(prototype) {      ['next', 'throw', 'return'].forEach(function (method) {          prototype[method] = function (arg) {              return this._invoke(method, arg);          };      });  }  // Gp在之前的原型操作有用到  defineIteratorMethods(Gp);  

然後當我們執行的時候,就會走到_invoke定義的invoke方法中:

function invoke(method, arg) {      // 狀態判斷,拋錯      if (state === GenStateExecuting) {          throw new Error('Generator is already running');      }        // 已完成,返回done狀態      if (state === GenStateCompleted) {          if (method === 'throw') {              throw arg;          }            // Be forgiving, per 25.3.3.3.3 of the spec:          // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume          return doneResult();      }        // 這裡就是之前定義的Context實例,下面代碼沒啥了,自己看吧      context.method = method;      context.arg = arg;        while (true) {          var delegate = context.delegate;          if (delegate) {              var delegateResult = maybeInvokeDelegate(delegate, context);              if (delegateResult) {                  if (delegateResult === ContinueSentinel) continue;                  return delegateResult;              }          }            if (context.method === 'next') {              // Setting context._sent for legacy support of Babel's              // function.sent implementation.              context.sent = context._sent = context.arg;          } else if (context.method === 'throw') {              if (state === GenStateSuspendedStart) {                  state = GenStateCompleted;                  throw context.arg;              }                context.dispatchException(context.arg);          } else if (context.method === 'return') {              context.abrupt('return', context.arg);          }            state = GenStateExecuting;            // innerFn就是while個循環了,使我們的代碼主體          var record = tryCatch(innerFn, self, context);            if (record.type === 'normal') {              // If an exception is thrown from innerFn, we leave state ===              // GenStateExecuting and loop back for another invocation.              state = context.done                  ? GenStateCompleted                  : GenStateSuspendedYield;                if (record.arg === ContinueSentinel) {                  continue;              }                return {                  value: record.arg,                  done: context.done              };          } else if (record.type === 'throw') {              state = GenStateCompleted;              // Dispatch the exception by looping back around to the              // context.dispatchException(context.arg) call above.              context.method = 'throw';              context.arg = record.arg;          }      }  };  

在之後,就是我們熟悉的promise相關操作了,在判斷done是否為true,否則繼續執行,將_next和_throw作為resolve和reject傳入即可。

小結

可以看到,僅僅一個async其實做了不少工作。核心就是兩個,產出一個兼容版本的generator和使用promise,回到這節的問題上,答案就是:

return new Promise(function (resolve, reject) {});  

沒錯,就是返回一個Promise,內部會根據狀態及決定是否繼續執行下一個Promise.resolve().then()。

如果async函數內有很多其他操作的代碼,那麼while會跟着變化,利用prev和next來管理執行順序。這裡就不具體分析了,自己寫個例子就明白了~

可以通過babel在線轉換,給自己一個具象的感知,更利於理解。

為什麼下面這種函數外的console不會等待,函數內的會等待?

async function fn() {      await (async () => {          await new Promise((r) => {              setTimeout(function () {                  r();              }, 2000);          });      })();      console.log('你好');  }  fn();  console.log(123);  

因為解析後的console.log(123); 是在整個語法糖之外啊,log 和 fn 是主協程序,fn內是輔協程。不相干的。

總結

有句話怎麼說來着,會者不難,難者不會。所以人人都是大牛,只是你還沒發力而已,哈哈~

筆者後來思考覺得這種寫法完全就是回調函數的替代品,而且增加了空間,加深了調用堆棧,或許原生的寫法才是效率最高的吧。

不過,需要良好的編碼規範,算是一種折中的方式了。畢竟用這種方式來寫業務事半功倍~

對於本文觀點,完全是個人閱讀後的思考,如有錯誤,歡迎指正,我會及時更新,避免誤導他人。

拜了個拜~