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

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

內容分爲翻譯和原創,若是有問題,歡迎隨時評論或私信,但願和你們一塊兒進步。html

分享不易,但願可以獲得你們的支持和關注。java

TL;DR

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

若是懶得往下看,能夠看下這個極其簡易版本的實現方式:es6

// 複製粘貼便可直接運行
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 和咱們的平常開發緊密相連,可是你真的瞭解其背後的原理嗎?github

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

簡述promise

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

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

具體實現原理,市面上有各類各樣的寫法,我就很少此一舉了。閉包

簡述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內是輔協程。不相干的。

總結

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

筆者後來思考以爲這種寫法徹底就是回調函數的替代品,並且增長了空間,加深了調用堆棧,或許原生的寫法纔是效率最高的吧。

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

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

拜了個拜~

相關文章
相關標籤/搜索