• 面试官: 说说你对async的理解


    大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。

    内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。

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

    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内是辅协程。不相干的。

    总结

    有句话怎么说来着,会者不难,难者不会。所以人人都是大牛,只是你还没发力而已,哈哈~

    笔者后来思考觉得这种写法完全就是回调函数的替代品,而且增加了空间,加深了调用堆栈,或许原生的写法才是效率最高的吧。

    不过,需要良好的编码规范,算是一种折中的方式了。毕竟用这种方式来写业务事半功倍~

    对于本文观点,完全是个人阅读后的思考,如有错误,欢迎指正,我会及时更新,避免误导他人。

    拜了个拜~

  • 相关阅读:
    牛客国庆集训派对Day6 Solution
    牛客国庆集训派对Day5 Solution
    牛客国庆集训派对Day4 Solution
    牛客国庆集训派对Day3 Solution
    牛客国庆集训派对Day2 Solution
    牛客国庆集训派对Day1 Solution
    The 2018 ACM-ICPC Asia Qingdao Regional Contest, Online Solution
    ACM-ICPC 2018 焦作赛区网络预赛 Solution
    2016 CCPC 长春 Solution
    CCPC 2017-2018, Finals Solution
  • 原文地址:https://www.cnblogs.com/xiaoyuxy/p/12682360.html
Copyright © 2020-2023  润新知