天天看點

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

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

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

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

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

TL;DR#

async是generator和promise的文法糖,利用疊代器的狀态機和promise來進行自更新!

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

Copy

// 複制粘貼即可直接運作

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());

這隻是一個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);
})           

);

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

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 的克隆品

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'

;

那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('你好');           

console.log(123);

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

總結#

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

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

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

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

拜了個拜~

作者: 小雨小雨丶

出處:

https://www.cnblogs.com/xiaoyuxy/p/12682360.html

繼續閱讀