扯扯 js 的async / await

zsx in 代码分析 / 1 / 5966

ES7有一个特性,便是async / await。以下便是个例子

async function a1() {

  return new Promise((resolve, reject) => {

    setTimeout(resolve, 1000);

  })

}

async function a2() {

  await a1();

  console.log("2333");

}


async / await,就我所知比较早的实现出于F#等函数式语言,后被C# 4.0所吸收。套用MSDN的流程图:

Trace an async program

其可以有效地梳理流程,避免过多的语法噪音(说你呢Promise)。不过当然了,其只适合命令式的调用,适合过程表达。要梳理流程的话,还是Promisethen链式调用更为合适——利用函数的组合来把整个流程表达清楚。另外,await / async会不可避免地用到try..catch,而try..catch内的代码是不会被JavaScript引擎所优化的。所以在这时用Promisereject来代为处理也比较合适。


先来一段 callback hell测试代码:

console.log("1");

setTimeout(() => {

    console.log("2");

    setTimeout(() => {

        console.log("3");

        setTimeout(() => {

            console.log("4");

        }, 3000)

    }, 2000);

}, 1000);


在我的记忆中,Wind.js是前端领域中最早利用async / await梳理流程的库。如上面这段代码,我们可以等价成(假设a1 == setTimeout)以下代码。

var A = Wind.compile("async", function () {

    console.log("1");

    $await(a1(1000));

    console.log("2");

    $await(a1(2000));

    console.log("3");

    $await(a1(3000));

    console.log("4");

    return 5;

});

其将被展开为以下代码:

* async << function () { */   (function () {

                                   var _builder_$0 = Wind.builders["async"];

                                   return _builder_$0.Start(this,

                                       _builder_$0.Delay(function () {

/*     console.log("1"); */                console.log("1");

/*     $await(a1(1000)); */                return _builder_$0.Bind(a1(1000), function () {

/*     console.log("2"); */                    console.log("2");

/*     $await(a1(2000)); */                    return _builder_$0.Bind(a1(2000), function () {

/*     console.log("3"); */                        console.log("3");

/*     $await(a1(3000)); */                        return _builder_$0.Bind(a1(3000), function () {

/*     console.log("4"); */                            console.log("4");

                                                       return _builder_$0.Return(5);

                                                   });

                                               });

                                           });

                                       })

                                   );

/* } */                        })

//@ sourceURL=wind/1_anonymous.js

作为一个较早的异步流程库,其缺陷有几:

  1. 原作者所说的JIT是在前端进行然后eval的,导致其代码不会被JavaScript引擎优化。(当然,预编译兴许能解决此问题)

  2. 语法噪音过多。

  3. try .. catch的大量使用(js引擎不优化try..catch内代码块)

  4. 有造成函数栈溢出的可能            

翻其代码。其Wind.builders.async.Start函数内创建了一个Task。该Task创建时的delegate将调用被.Delay包装的函数。

        Delay: function (generator) {

            return {

                next: function (_this, callback) {

                    try {

                        var step = generator.call(_this);

                        step.next(_this, callback);

                    } catch (ex) {

                        callback("throw", ex);

                    }

                }

            };

        },

该函数的同步部分执行完毕后,返回另一个被.Bind包装的函数。其亦拥有一个next方法。

        Bind: function (task, generator) {

            return {

                next: function (_this, callback) {

                    

                    var onComplete = function () {

                        if (this.error) {

                            callback("throw", this.error);

                        } else {

                            var nextTask;

                            try {

                                nextTask = generator.call(_this, this.result);

                            } catch (ex) {

                                callback("throw", ex);

                                return;

                            }



                            nextTask.next(_this, callback);

                        }

                    }



                    if (task.status == "ready") {

                        task.addEventListener("complete", onComplete);

                        task.start();

                    } else if (task.status == "running") {

                        task.addEventListener("complete", onComplete);

                    } else {

                        onComplete.call(task);

                    }

                }

            };

        }

在Wind内建的事件机制的帮助下,该方法将在当前task完成后运行。等其运行完毕,得到下一个被包装的函数并执行。一直到最后调用了Return.nextcallback回去,流程结束。

这个思想,似乎就是generator的思想。    


我们看看现代的transformer吧。

async function ping() {

    for (var i = 0; i < 10; i++) {

        await delay(300);

        console.log("ping");

    }

}



function delay(ms) {

    return new Promise(resolve => setTimeout(resolve, ms));

}

先看看会转到es5的babel是怎么实现async / await的。

function ping() {

    var i;

    return regeneratorRuntime.async(function ping$(context$1$0) {

        while (1) switch (context$1$0.prev = context$1$0.next) {

            case 0:

                i = 0;

            case 1:

                if (!(i < 10)) {

                    context$1$0.next = 8;

                    break;

                }



                context$1$0.next = 4;

                return regeneratorRuntime.awrap(delay(300));



            case 4:

                console.log("ping");



            case 5:

                i++;

                context$1$0.next = 1;

                break;



            case 8:

            case "end":

                return context$1$0.stop();

        }

    }, null, this);

}



function delay(ms) {

    return new Promise(function (resolve) {

        return setTimeout(resolve, ms);

    });

}

通过这样的代码,我们发现以下事实:babel维护了一个状态机,然后通过regeneratorRuntime来运行代码。

看看TypeScript Compiler的实现(to ES6):

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

   return new (P || (P = Promise))(function (resolve, reject) {

       function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }

       function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } }

       function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }

       step((generator = generator.apply(thisArg, _arguments)).next());

   });

};

function ping() {

   return __awaiter(this, void 0, void 0, function* () {

       for (var i = 0; i < 10; i++) {

           yield delay(300);

           console.log("ping");

       }

   });

}

function delay(ms) {

   return new Promise(resolve => setTimeout(resolve, ms));

}

regeneratorRuntimegenerator的运行时实现之一,而__awaiter也是generator的包装。当然,后者的generator是使用引擎的,前者的是用js模拟的。根据MSDN博客,他们表示,要让tsc支持async -> es5的话,也要实现一个基于状态机的generator

最后看看Chakra引擎内建的实现。可以看到的是,其await实现与yield实现类似。yield和await在token解析时大多相邻甚至写在一起,最后到下面那个函数都以类型Js::OpCode::Yield进行处理。所以整块Chakra中,出现await的基本只有Parser部分。比如ByteCodeEmitter::Emit内:    

    case knopAwait:

    case knopYield:

        byteCodeGenerator->StartStatement(pnode);

        funcInfo->AcquireLoc(pnode);

        Emit(pnode->sxUni.pnode1, byteCodeGenerator, funcInfo, false);

        EmitYield(pnode->sxUni.pnode1->location, pnode->location, byteCodeGenerator, funcInfo);

        funcInfo->ReleaseLoc(pnode->sxUni.pnode1);

        byteCodeGenerator->EndStatement(pnode);

        break;

await直接被当做yield执行(当然到这里的代码已经经过了其他处理)。


嘛,总之await / async就是层generator的语法糖嘛。看看规范是怎么脱糖的:

function spawn(genF, self) {

    return new Promise(function(resolve, reject) {

        var gen = genF.call(self);

        function step(nextF) {

            var next;

            try {

                next = nextF();

            } catch(e) {

                // finished with failure, reject the promise

                reject(e);

                return;

            }

            if(next.done) {

                // finished with success, resolve the promise

                resolve(next.value);

                return;

            }

            // not finished, chain off the yielded promise and `step` again

            Promise.resolve(next.value).then(function(v) {

                step(function() { return gen.next(v); });

            }, function(e) {

                step(function() { return gen.throw(e); });

            });

        }

        step(function() { return gen.next(undefined); });

    });

}

这糖挺甜的,不是吗(笑

又水了一篇没意义的文章呢,明明翻个规范什么都解决了。总结:

async / await === Promise + generator + yield


参考代码:    

  1. https://github.com/JeffreyZhao/wind

  2. https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-async-to-generator

  3. https://github.com/babel/babel/tree/master/packages/babel-regenerator-runtime/

  4. https://github.com/Microsoft/ChakraCore

参考文章:

  1. Asynchronous Programming with Async and Await (C# and Visual Basic)

  2. Asynchronous Workflows (F#)

  3. What about Async/Await?

  4. Asynchronous code gets easier with ES2016 Async Function support in Chakra and Microsoft Edge

  5. Async Functions            

  6. async & await 的前世今生(Updated)

  7. 用ES6 Generator替代回调函数

  8. 浅谈尾递归的优化方式            

如果本文对你有帮助,你可以用支付宝支持一下:

Alipay QrCode