27

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

 4 years ago
source link: http://www.cnblogs.com/xiaoyuxy/p/12682360.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

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

有兴趣的朋友可以再看看这篇 文章

然后,我们再来看看这张图,跟着箭头走一遍,是不是就很清晰了?

QRR7Rvv.png!web

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

总结

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

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

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

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

拜了个拜~

3MbaaaI.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK