JavaScript Event Loop, Callback, Promise, Generator, async/await

Table of Contents

1 异步:现在和将来的代码块

1.1 分块的程序

可以把JavaScript程序看作由多个块(Chunks)构成的。 这些块中只有一个是现在执行,其余的则会在将来执行。 最常见的块单位是函数。

'use strict';

console.log("begin");

function foo() {
    console.log("This is foo");
}
function bar() {
    console.log("This is bar");
}

setTimeout(foo, 1000);
setTimeout(bar, 1000);

上面代码由3块组成:
块1:主程序

console.log("begin");

setTimeout(foo, 1000);
setTimeout(bar, 1000);

块2:函数foo

    console.log("This is foo");

块3:函数bar

    console.log("This is bar");

其中,块1是现在执行,块2和块3会在将来执行。

1.2 JavaScript运行时概念模型

JavaScript运行时概念模型如图 1 所示。

js_event_loop_runtime_concepts.png

Figure 1: JavaScript运行时概念模型

栈(Stack):函数调用形成了一个栈帧。
堆(Heap):对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。
队列(Queue):一个JavaScript运行时包含了一个待处理的消息队列(又称“事件队列”)。每一个消息都与一个函数(称为“回调函数”)相关联。 当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着这个消息处理结束,接着可以处理下一个消息了。这就是“事件循环”的过程。

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

1.3 事件循环

上一节已经介绍了事件循环的过程。下面通过一段伪代码(摘自“You Don't Know JS: Async & Performance”)来说明这个概念:

// 事件循环极度简化的伪代码描述

var eventLoop = [ ];   // eventLoop是一个用作队列的数组(先进,先出)
var event;

// “永远”执行
while (true) {
    // 一次tick
    if (eventLoop.length > 0) {
        // 拿到队列中的下一个事件
        event = eventLoop.shift();
        // 现在,执行下一个事件(事件关联的函数)
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

这当然是一段极度简化的伪代码,只用来说明概念。不过它应该足以用来帮助大家有更好的理解。

在上面“永远”执行的while循环中每一轮循环称为一个tick。对每个tick而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行其关联的回调函数。

可以使用setTimeout()等方法把回调函数挂在事件循环队列中。不过,需要说明的是, setTimeout()并没有把你的回调函数直接挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的tick会摘下并执行这个回调函数。

除setTimeout()等方法外,很多事件上可以注册回调函数。当事件发生时,环境会把你的回调函数放在事件循环中。如:

var link=document.getElementById("mylink");
link.onclick=function(){       // 注册onclick事件的回调函数。onclick事件发生时会把回调函数挂在事件循环队列中。
    console.log("I was clicked !");
};

1.4 JavaScript是单线程的

JavaScript是单线程的(注:是指用户代码是单线程的,JavaScript引擎往往不是单线程的)。

JavaScript之所以被设计为单线程,与它的主要用途有关。发明Javascript时,它的主要用途是与用户交互、操作DOM等。假设JavaScript不是单线程的,比如JavaScript有两个执行线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?如果把这个同步的责任交给用户,则用户代码将变得复杂。 为了避免这种复杂性,JavaScript被设计为单线程。

注:为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript创建多个worker线程,不过Web Worker有很多限制,如:Web Worker无法访问DOM节点;Web Worker无法访问全局变量或全局函数等等。

1.4.1 完整运行(run-to-completion)

考虑下面例子:

var a = 1;
var b = 2;

function foo() {
    a++;
    b = b * a;
    a = b + 3;
}

function bar() {
    b--;
    a = 8 + b;
    b = a * 2;
}

// ajax(...)是某个库中提供的某个Ajax函数。由于网络响应时间不确定,所以foo和bar的执行顺序是不确定的
ajax("http://some.url.1", foo);
ajax("http://some.url.2", bar);

由于JavaScript的单线程特性,foo()/bar()中的代码具有原子性。也就是说,假设 “http://some.url.1” 响应更快,foo()会先执行 一旦foo()开始运行,它的所有代码都会在bar()中的任意代码运行之前完成。比如,假设foo()要执行1分钟,在这1分钟内 “http://some.url.2” 响应完成了,那么bar()并不会打断foo()的执行。这称为完整运行(run-to-completion)特性。

由于foo()不会被bar()中断,bar()也不会被foo()中断,所以这个程序只有两个可能的结果,这取决于这两个函数哪个先运行,如果foo()先运行则结果为a=11,b=22,如果bar()先运行则结果为a=183,b=180(可以自行验证)。如果存在多线程,且foo()和bar()中的语句可以交替运行的话,可能的结果将会增加很多(请考虑一下其它语言中的多线程同步问题)。

这种不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别(或者说,表达式运算顺序级别)。换句话说,JavaScript单线程事件模型的确定性要远远高于多线程情况。

1.4.2 “完整运行”实例

看下面例子:

'use strict';

console.log("begin");

function A() {
    console.log("This is A");
}

setTimeout(A, 0);         // 0毫秒后执行函数A

console.log("end");

由于JavaScript是单线程的,具有“完整运行”特性。上面代码中,一定会先输出字符串“begin”和“end”(它们都在主程序中),然后输出字符串“This is A”。

下面例子更能说明问题。

'use strict';
var execSync = require('child_process').execSync;

console.log("begin");

function A() {
    console.log("This is A");
}

function longTimeTask(second) {          // js中没有sleep函数,这里用execSync来模拟一个长时间的任务
    let cmd = `bash -c "sleep ${second}"`;
    console.log(`This is long task, exec ${cmd}`);
    execSync(cmd);
}

setTimeout(A, 0);          // 0秒后执行函数A

longTimeTask(2);           // 这是一个2秒的任务(注:JavaScrip中应该避免这样使用!)

console.log("end");

上面代码中的注释“0秒后执行函数A”是不够准确的,准确的说法是0秒后,把回调函数A放在事件循环中,下一个tick(前面介绍过tick的概念)时会从事件循环队列中取出回调函数A,并执行。

执行上面代码,会输出:

begin
This is long task, exec bash -c "sleep 2"
end
This is A

从输出中可知,尽管longTimeTask会执行2秒(长于给函数A设置的超时器时间0秒),函数A也不会在这2秒内执行,这是因为Java是单线程的,具有“完整运行”特性。

注:如果longTimeTask的执行需要100秒(这在JavaScript中是应该避免的!),则“This is A”至少要等100秒才有可能被输出(这显然和setTimeout的初衷不符)。

从这个例子可以看出: JavaScrpt不适合CPU密集型的任务,适合I/O密集型的任务。

1.5 并发中的交互:gate


考虑下面代码:

// 我们期望两个ajax请求都完成后(即foo,bar都被调用后),再通过baz输出a+b的值
// 下面演示代码是错误的,baz会调用两次,只有最后一次的结果是正确的

var a, b;

function foo(x) {
    a = x * 2;
    baz();
}

function bar(y) {
    b = y * 2;
    baz();
}

function baz() {
    console.log(a + b);
}

// ajax(..)是某个库中的某个Ajax函数
ajax("http://some.url.1", foo);
ajax("http://some.url.2", bar);

如何实现两个ajax请求都完成后,再调用baz呢?请看下面代码:

// 我们期望两个ajax请求都完成后(即foo,bar都被调用后),再通过baz输出a+b的值
// 使用gate后,可以保证两个ajax请求都完成后再调用一次baz

var a, b;

function foo(x) {
    a = x * 2;
    if (a && b) {       // if (a && b) 被称为gate,这里写为 if (b) 也行
        baz();
    }
}

function bar(y) {
    b = y * 2;
    if (a && b) {       // if (a && b) 被称为gate,这里写为 if (a) 也行
        baz();
    }
}

function baz() {
    console.log(a + b);
}

// ajax(..)是某个库中的某个Ajax函数
ajax("http://some.url.1", foo);
ajax("http://some.url.2", bar);

包裹baz调用的条件判断 if (a && b) 称为门(gate),我们虽然不能确定a和b到达的顺序,但是会等到它们两个都准备好再进一步打开门(调用baz)。

1.6 并发中的交互:latch


考虑下面代码:

// 我们期望两个ajax请求中的其中一个完成后,就通过baz输出a的值
// 下面演示代码是错误的,baz会调用两次,但我们只想要第一次的输出结果

var a;

function foo(x) {
    a = x * 2;
    baz();
}

function bar(x) {
    a = x / 2;
    baz();
}

function baz() {
    console.log( a );
}

// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

如何实现两个ajax请求中的其中一个完成后,就通过baz输出a的值呢?请看下面代码:

// 我们期望两个ajax请求中的其中一个完成后,就通过baz输出a的值
// 使用latch后,可以保证仅第一个ajax请求完成后,输出对应a值

var a;

function foo(x) {
    if (!a) {            // if (!a) 被称为latch
        a = x * 2;
        baz();
    }
}

function bar(x) {
    if (!a) {            // if (!a) 被称为latch
        a = x / 2;
        baz();
    }
}

function baz() {
    console.log( a );
}

// ajax(..)是某个库中的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

条件判断 if (!a) 称为门(latch),它使得只有foo和bar中的第一个可以通过,第二个(实际上是任何后续的)调用会被忽略。也就是说,第二名没有任何意义!

2 回调函数

回调函数是JavaScript中最基本的异步模式。

考虑下面这段代码:

// A
setTimeout(function(){
// C
}, 1000 );
// B

其中,代码“// C”部分就是回调函数的内容。

2.1 问题一:回调地狱(缺乏顺序性)

考虑下面代码:

// 回调地狱,代码难以理解、追踪、调试和维护!
listen( "click", function handler(evt) {
    setTimeout( function request() {
        ajax( "http://some.url.1", function response(text){
            if (text == "hello") {
                handler();
            }
            else if (text == "world") {
                request();
            }
        } );
    }, 500) ;
} );

这种代码常常被称为回调地狱(callback hell),有时也被称为毁灭金字塔(pyramid of doom,得名于嵌套缩进产生的横向三角形状)。

这样的代码和人大脑的顺序思考问题的方式相违背,这使得代码变得更加难以理解、追踪、调试和维护。
不过,“横向三角形状”只是一个表相,我们可以把上面代码改写为:

// 尽管不是“横向三角形状”代码,但也难以理解、追踪、调试和维护!
listen( "click", handler );

function handler() {
    setTimeout( request, 500 );
}

function request(){
    ajax( "http://some.url.1", response );
}

function response(text){
    if (text == "hello") {
        handler();
    }
    else if (text == "world") {
        request();
    }
}

上面代码没有“横向三角形状”式的缩进,但它也是“回调地狱”,代码难以理解、追踪、调试和维护!

2.2 问题二:信任问题(控制反转问题)


考虑下面代码:

// A
ajax( "...", function(...){
    // C
});
// B

代码“// A”和“// B”发生于现在,在JavaScript主程序的直接控制之下。而代码“// C”会延迟到将来发生,并且是在第三方的控制下——在本例中就是函数ajax(…)。我们把这称为“控制反转(inversion of control)”,也就是把自己程序一部分的执行控制交给某个第三方,但第三方不一定可靠!

下面将通过一个具体的例子来说明回调的信任问题。

假设你是一名开发人员,为某个销售昂贵电视的网站建立商务结账系统。你已经做好了结账系统的各个界面。在最后一页,当用户点击“确定”就可以购买电视时,你需要调用(假设由某个分析追踪公司提供的)第三方函数以便跟踪这个交易。

可能是为了提高性能,他们提供了一个看似用于异步追踪的工具,这意味着你需要传入一个回调函数。在这个函数中你需要提供向客户收费和展示感谢页面的最终代码。

代码可能是这样:

analytics.trackPurchase( purchaseData, function(){
    chargeCreditCard();
    displayThankyouPage();
});

很简单,是不是?你写好代码,通过测试,一切正常,然后就进行产品部署。皆大欢喜!

突然,某一天你的老板惊慌失措地打电话让你赶紧到办公室,说你们的一位高级客户购买了一台电视,信用卡却被刷了五次!老板生气地说:“这种情况你没有测试过吗?!”

通过分析日志,你得出一个结论:唯一的解释就是那个第三方分析工具出于某种原因把你的回调函数调用了五次而不是一次。沮丧的你联系他们的客服,而客服显然和你一样吃惊。他们保证,一定会向开发者提交此事,之后再给你回复。第二天,你收到一封很长的信,信中他们确定了确实是他们的问题,且充满了歉意,并保证绝不会再发生同样的事故等等。

你和老板讨论此事,老板说我们不能再信任他们了。你需要找到某种方法来保护结账代码,保证不再出问题。经过思考后,你实现了像下面这样的代码:

var tracked = false;

analytics.trackPurchase( purchaseData, function(){
    if (!tracked) {
        tracked = true;
        chargeCreditCard();
        displayThankyouPage();
    }
});

不过,后来有一个QA工程师问道:“如果他们根本不调用这个回调函数怎么办?”

然后,你开始沿着这个兔子洞深挖下去,考虑着他们调用你的回调时所有可能的出错情况。这里粗略列出了你能想到的分析工具可能出错的情况:
• 调用回调过早(在追踪之前);
• 调用回调过晚(或没有调用);
• 调用回调的次数太少或太多(就像你遇到过的问题!);
• 没有把所需的环境/参数成功传给你的回调函数;
• 吞掉可能出现的错误或异常;
• ……

现在你应该更加明白回调地狱是多像地狱了吧。

3 Promise

通过前面内容可知, 通过回调函数来表达程序异步模式和管理并发存在两个主要缺陷:缺乏顺序性和可信任性。 使用 Promise 可以解决这两个缺陷。

Promise中链式的then调用比回调函数看起来更具有顺序性,代码更易维护。Promise更深刻的好处在于它解决了回调函数存在的信任问题。Promise没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。

3.1 Promise可解决信任问题

在节 2.2 中介绍过,回调函数存在严重的信任问题。如把一个回调函数传入工具foo(…)时可能出现如下问题:
• 调用回调过早;
• 调用回调过晚(或没有调用);
• 调用回调的次数太少或太多;
• 没有把所需的环境/参数成功传给你的回调函数;
• 吞掉可能出现的错误或异常;
• ……

下面解释为什么Promise可以防止回调被“过多”地调用。根据Promise的定义,Promise对象的状态一旦改变,就不会再变化。Promise创建代码试图调用resolve(…)或reject(…) 多次,或者试图两者都调用,那么这个Promise将只会接受第一次决议(决议就是状态改变过程),并默默地忽略任何后续调用。

关于Promise解决其它信任问题的说明可参考:You Don't Know JS: Async & Performance, 3.3 Promise Trust

3.2 Promise模式

3.2.1 Promise.all(可取代gate)

Promise.all()方法将多个Promise实例包装为新的Promise实例。

下面是Promise.all()的应用实例:

// add接收两个Promise,返回Promise
function add(xPromise, yPromise) {
    // Promise.all([ .. ])接受一个promise数组并返回一个新的promise,
    // 这个新promise等待数组中的所有promise完成
    return Promise.all( [xPromise, yPromise] )
        .then( function(values){           // 这个promise决议后,我们把收到的X和Y值加一起
            return values[0] + values[1];  // values是来自于之前决议的promise的消息数组
        } );
}

add(fetchX(), fetchY())        // fetchX(), fetchY()是返回Promise的函数
    .then( function(sum) {
        console.log(sum);
    });

如果不使用Promise,使用传统的回调函数机制,我们需要使用gate的技巧。其代码类似于(节 1.5 有类似的代码):

// 用传统的回调
function add(getX,getY,cb) {
    var x, y;

    getX( function(xVal){
        x = xVal;
        // 两个都准备好了?
        if (y != undefined) {        // 这里是gate,确保两个数据都准备好了才调用cb
            cb( x + y );
        }
    } );

    getY( function(yVal){
        y = yVal;
        // 两个都准备好了?
        if (x != undefined) {        // 这里是gate,确保两个数据都准备好了才调用cb
            cb( x + y );
        }
    } );
}

add(fetchX, fetchY, function(sum){
    console.log(sum);
});

在经典的编程术语中,门(gate)是这样一种机制要等待两个或更多并行/并发的任务都完成才能继续。它们的完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制继续。

3.2.2 Promise.race(可取代latch)

尽管Promise.all()协调多个并发Promise的运行,并假定所有Promise 都需要完成,但有时候你会想只响应“第一个跨过终点线的Promise”,而抛弃其他Promise。这种模式传统上称为门闩(latch),但在Promise中称为竞态(race)。

Promise.race()也接受单个数组参数。这个数组由一个或多个Promise,一旦有任何一个Promise决议为完成,Promise.race()就会完成;一旦有任何一个Promise决议为拒绝,它就会拒绝。

下面是Promise.race的例子:

// request(..)是一个支持Promise的Ajax工具,即它返回Promise
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
    .then( function(msg){
        // p1或者p2将赢得这场竞赛
        console.log( msg );
    } );

如果不使用Promise,使用传统的回调函数机制,我们需要使用latch的技巧,可以参考节 1.6 ,这里不详细写代码。

3.2.3 Promise.all和Promise.race的变体

Promise.all()和Promise.race()还有其他几个常用的变体模式:

none()
这个模式类似于all(),不过完成和拒绝的情况互换了。所有的Promise都要被拒绝,即拒绝转化为完成值,反之亦然。
any()
这个模式与all() 类似,但是会忽略拒绝,所以只需要完成一个而不是全部。
first()
这个模式类似于与any()的竞争,即只要第一个Promise完成,它就会忽略后续的任何拒绝和完成。
last()
这个模式类似于first(),但却是只有最后一个完成胜出。

3.3 Job Queue

考虑下面代码:

console.log("A");

setTimeout(() => {
  console.log("A - setTimeout 1");
}, 0);

new Promise((resolve) => {
  console.log("Promise 1, A");
  resolve();
})
.then(() => {
  return console.log("Promise 1, B");
});

console.log("AAA");

它的输出为:

A
Promise 1, A
AAA
Promise 1, B
A - setTimeout 1

由于Promise的参数会同步执行,所以下面三行会先输出:

A
Promise 1, A
AAA

由于Promise.then的中的任务放在Job Queue中,Job Queue中的任务比Event Loop Queue中的任务的优先级高,所以“Promise 1, B”比“A - setTimeout 1”先输出。

注:ES6中新增了Job Queue的概念,它与Promise的执行有关,可以理解为等待执行的任务。 Job Queue中的任务比Event Loop Queue中的任务的优先级高。

Job Queue与Event Loop Queue的相同点:都是先进先出队列。
Job Queue与Event Loop Queue的不同点:
(1). 每个JavaScript Runtime可以有多个Job Queue,但只有一个Event Loop Queue。
(2). 当JavaScript Engine处理完当前块后,优先执行所有的Job Queue,然后再处理Event Loop Queue。

考虑下面代码:

console.log("A");

setTimeout(() => {
  console.log("A - setTimeout 1");
}, 0);

setTimeout(() => {
  console.log("A - setTimeout 2");
}, 0);

new Promise((resolve) => {
  console.log("Promise 1, A");
  resolve();
})
.then(() => {
  return console.log("Promise 1, B");
})
.then(() => {
  return console.log("Promise 1, C");
});

new Promise((resolve) => {
  console.log("Promise 2, A");
  resolve();
})
.then(() => {
  return console.log("Promise 2, B");
})
.then(() => {
  return console.log("Promise 2, C");
})
.then(() => {
  return console.log("Promise 2, D");
});

console.log("AAA");

它的输出为:

A
Promise 1, A
Promise 2, A
AAA
Promise 1, B
Promise 2, B
Promise 1, C
Promise 2, C
Promise 2, D
A - setTimeout 1
A - setTimeout 2

理解这个输出:
前面4行最先输出,因为它们不是异步任务,属于第一个chunk。
Promise 1与Promise 2先于setTimeout执行,因为Job Queue的执行优先于Event Loop Queue。
Promise 1与Promise 2各自的输出都是顺序的,因为Job Queue是先进先出队列,同一Job Queue中的任务顺序执行。
Promise 1与Promise 2的后续任务是交错的,因为Promise 1与Promise 2都是独立的PromiseJob(Job 的其中一种),属于不同的Job Queue,它们之间的顺序规范中没有规定。

参考:https://zhuanlan.zhihu.com/p/22710155

4 Generator

Javascript中的生成器(Generator)和Python中的生成器很相似。

Javascript中Generator的语法是在关键字 function 和生成器名字之间增加一个星号(*)。如:

// Javascript中Generator简单例子

function* idMaker() {          // 星号靠近函数名也可以,即function *idMaker() {
    console.log("begin idMaker");
    var index = 0;
    while(true) {
        yield index++;
    }
}

var gen = idMaker();           // 返回迭代器,在迭代器上调用next时,Generator会执行到遇到yield或者return。

console.log("call next 1st");
var res=gen.next();            // next()调用的结果是一个对象,它有一个value属性,保存着idMaker返回的值(yield后的表达式)
console.log(res.value);        // 输出 0

console.log("call next 2nd");
res=gen.next();
console.log(res.value);        // 输出 1

console.log("call next 3rd, and more");
console.log(gen.next().value); // 输出 2
console.log(gen.next().value); // 输出 3
console.log(gen.next().value); // 输出 4

上面代码会输出:

call next 1st
begin idMaker
0
call next 2nd
1
call next 3rd, and more
2
3
4

下面再看一个Generator的简单例子:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

console.log(g.next().value);
console.log(g.next().value);
console.log(g.next().value);
console.log(g.next().value);

上面代码会输出:

1
2
3
undefined

4.1 传递参数

yield 表达式本身没有返回值,或者说总是返回undefined。 next() 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。如:

function* gen() {
    let a = yield 1;
    let b = yield (2 + a);
    yield (3 + b);
}

var g = gen();

console.log(g.next().value);       // 第一次调用next时,传递参数是无效的(会被忽略)
console.log(g.next(100).value);    // 这使得上一个yield表达式的返回值为100,即a为100
console.log(g.next(200).value);    // 这使得上一个yield表达式的返回值为200,即b为200

上面代码会输出:

1
102
203

由于 next() 方法的参数表示上一个 yield 表达式的返回值,所以在第一次使用 next() 方法时,传递参数是无效的(会被忽略)。从语义上讲,第一个 next() 方法用来启动遍历器对象,所以不用带有参数。

5 async/await

从字面意思看成,async是“异步”的简写,而await可以认为是“async wait”的简写。除Javascript外,async/await在其他语言(如C#和Python 3.5)中已经被支持。

async/await是ES2017中提出的,本节代码的测试在nodejs 8+中完成。

5.1 async

关键字 async 用于声明一个函数是异步的(这个异步函数执行时会返回一个Promise)。

async function name([param[, param[, ... param]]]) {
   statements
}

下面是 async 的例子:

async function testAsync() {
    return 1;
}

const result = testAsync();     // result是一个Promise

result.then(v => {
  console.log(v);               // 输出1
});

5.2 await

操作符 await 用于等待一个异步函数执行完成, await 操作符只能用在 async 声明的函数内。

[rv] = await expression;

其中,“expression”是A Promise or any value to wait for the resolution. 而返回值“rv”是resolved value of the promise, or the value itself if it's not a Promise.

下面是async/await的例子:

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function add(x) {
  var a = await resolveAfter2Seconds(20);     // 在这个Promise决议前,后面代码不会被执行
  var b = await resolveAfter2Seconds(30);     // 在这个Promise决议前,后面代码不会被执行
  return x + a + b;
}

add(10).then(v => {
  console.log(v);                    // prints 60 after 4 seconds.
});

async function add1(x) {
  var a = resolveAfter2Seconds(20);
  var b = resolveAfter2Seconds(30);
  return x + await a + await b;      // 在两个await等待的两个Promise决议前,后面代码(return语句)不会被执行
}

add1(10).then(v => {
  console.log(v);                    // prints 60 after 2 seconds.
});

5.3 用async/await代替Promise代码

假设有下面代码:

function getProcessedData(url) {
  return downloadData(url)               // returns a promise
    .catch(e => {
      return downloadFallbackData(url);  // returns a promise
    })
    .then(v => {
      return processDataInWorker(v);     // returns a promise
    });
}

上面代码可以改写为async/await的形式:

async function getProcessedData(url) {
  let v;
  try {
    v = await downloadData(url);
  } catch(e) {
    v = await downloadFallbackData(url);
  }
  return processDataInWorker(v);
}

改写完了以后可以发现, async/await可以使代码流程更加清晰,使“异步”代码看起来更像“顺序执行”的代码。

5.3.1 实例:回调、Promise和async/await三种写法的比较

下面是模拟一个Mongo数据库的操作的三种写法:回调、Promise和async/await。

// 方式一:回调写法
mongoDb.open(function(err, db) {
    if(!err) {
        db.collection("users", function(err, collection) {
            if(!err) {
                let person = {name: "yika", age: 20};
                collection.insert(person, function(err, result) {
                    if (!err) {
                        console.log(result);
                    }
                });
            }
        })
    }
});
// 方式二:Promise写法
let person = {name: "yika"};
mongoDb
    .open()
    .then(function(database) {
      return database.collection("users");
    })
    .then(function(collection) {
      return collection.insert(person);
    })
    .then(function(result) {
      console.log(result);
    })
    .catch(function(e) {
      throw new Error(e);
    })
// 方式三:async/await写法
async function insertData(person){
    let db, collection, result;
    try {
        db = await mongoDb.open();
        collection = await db.collection("users");
        result = await collection.insert(person);
    } catch(e) {
        console.error(e.message);
    }
    console.log(result);
}
insertData({name: "yika"});

从例子中可以看出,async/await写法使Javascript更加接近于同步编程的风格。

这个例子摘自:http://www.cnblogs.com/YikaJ/p/4996174.html

6 参考

You Don't Know JS: Async & Performance


Author: cig01

Created: <2016-08-28 Sun 00:00>

Last updated: <2017-12-27 Wed 22:32>

Creator: Emacs 25.3.1 (Org mode 9.1.4)