记得2015年看一些node的代码时,示例中往往会使用回调函数做一些异步处理,去年的时候看会发现大部分成了promise,当今年再看时发现大部分已经成了async/await。为什么js的异步操作解决方案更新的如此之快,每种方案的使用场景以及弊端是什么,下文中给出了介绍。以下均为自己的一些理解,描述如有偏差欢迎指正。

JS中异步和同步的概念

什么是同步呢?

1
2
3
4
function print() {
console.log('hi, javascript.');
}
print(); // hi,javascript.

如上述代码所示,调用该函数后不通过任何其他手段就能得到该代码运行的结果。

什么是异步呢?

1
2
3
4
5
var fs = require('fs');
fs.readFile('./image.png', function (err, buffer) {
if (err) throw err;
process(buffer);
});

如上述代码所示,调用readFile后并不能立即获得代码运行的结果,需要通过一定手段才能间接获取到代码运行的结果。上述代码中采用了 回调函数 机制获取到了代码运行后的结果,并进行接下来的操作。

综上,个人理解,如果函数运行后如果无法直接获取到程序运行的结果,还需要其他手段辅助才能获取到程序运行结果的便可以称之为异步。否则便是该函数则是一个同步的过程。

JS 中的异步编程

为什么需要异步呢?

JS 的执行环境为单线程,在单线程的执行环境下通过异步编程的方式可以提高程序执行的效率,避免同步方式带来的等待问题。

阮老的教程中总结到了在js中异步编程的几种方式,这篇文章的时间是2012年,目前前端新技术的涌现使得在js中处理异步的过程已经远远不止这几种方式。下面从 回调函数、promise、generator+co、async/await 四种方式下对异步的处理来解释js下的异步编程处理。

回调函数

上述对于异步的阐述中示例代码给出了回调函数的一个示例,通过传入一个回调函数获取到程序运行的结果。

什么是回调函数呢?

In computer programming, a callback is a reference to a piece of executable code that is passed as an argument to other code. 来自维基百科的定义

在js中回调函数指的是将待执行的函数A作为参数传递到另一个函数B中,在函数B中执行函数A。A称之为回调函数,如果传入的是一个函数表达式(没有函数名称),称之为匿名回调函数。

segmentfault上看到一个很经典的关于回调的比喻:

你有事去隔壁寝室找同学,发现人不在,你怎么办呢?
方法1,每隔几分钟再去趟隔壁寝室,看人在不
方法2,拜托与他同寝室的人,看到他回来时叫一下你
前者是轮询,后者是回调。
那你说,我直接在隔壁寝室等到同学回来可以吗?
可以啊,只不过这样原本你可以省下时间做其他事,现在必须浪费在等待上了。把原来的非阻塞的异步调用变成了阻塞的同步调用。
JavaScript的回调是在异步调用场景下使用的,使用回调性能好于轮询。

回调函数实践中的回调地狱问题(callback hell ):

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try { // 此处的try/catch 无法捕获内部回调函数的错误信息
connection.query(sql, (err, result) => {
if(err) {
// 此处如果抛出错误的话,无法被外层的try/catch捕获
console.err(err)
} else {
connection.query(sql, (err, result) => {
if(err) {
console.err(err)
} else {
...
}
})
}
})
} catch (e) {
console.log(e);
}

上述代码当回调函数层级过多时会出现回调地狱问题,其主要弊端有:

  • 代码可读性差,不利于后期的修改维护
  • 外部无法捕获回调函数内部抛出的错误(鉴于此可能会导致回调函数被执行多次)

Promise

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

上述是ECMA对Promise的定义。其内部定义并保存了一个异步操作的结果,该结果在Promise对象创建时是未知的。通过对Promise对象代理的异步过程的执行结果的成功和失败绑定对应的处理方法,我们可以按照同步的方式书写异步代码。

Promise的内部保存有三种状态:

  • pending 状态:代表初始状态
  • fulfilled 状态:代表操作已经成功完成
  • rejected 状态:代表操作失败

Promise通过级联式的api调用代替了回调函数的层层嵌套,如下所示:

1
2
3
4
5
6
7
8
var a = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('1')
}, 2000)
})
a.then(function(val) {
console.log(val)
})

Promise的错误处理机制,可以避免回调函数在嵌套中无法被外部错误机制捕获的问题。如下所示:

1
2
3
4
5
6
7
new Promise(function(resolve, reject) {
1 ? resolve() : reject()
}).then(function() {
console.log('resolve');
}).catch(function(err) {
console.log('reject');
})

最后的catch会捕获整个promise链中抛出的错误。具体Promise的用法可见阮老师的Promise 对象一节。

Promise 较之回调函数有以下好处:

  • 代码结构清晰
  • 可以捕获流程内部抛出的错误信息

Generator+co

ES2015中出现了Generator语法,随着Generator的出现,这一异步编程解决方案可以让我们更近乎同步的方式写代码,在代码结构上相对promise更友好。

什么是Generator?

阮大是这样说的: Generator是一个状态机,其内部封装了多个状态,在执行Generator函数时,会返回一个遍历器对象,通过改对象,依次遍历Generator函数内部的每一个状态。

1
2
3
4
5
6
7
function *asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
var g = asyncJob();
g.next();

如上述所示,代码中给出了使用Generator函数的示例。其中,Generator的function与函数名之间有一个*,函数内部使用yield关键词,定义不同的状态。

Generator函数中封装了不同的状态,通过next的调用可以依次遍历到所有的状态,如何自动管理generator的流程。业内较为出名是TJ大神的co模块,co模块用于Generator函数的自动执行。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 2000)
});
};

var b = co(function *() {
var val = yield a();
console.log(val)
})

b()

co模块的具体原理详见:co模块

async/await

ES2016草案中出现了async/await异步处理方案,其在语言层面解决JavaScript的异步回调问题。其使用方式和generator+co 的方式很像,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
}

var b = async function() {
var val = await a()
console.log(val)
}

b()

async/await机制有着以下特点:

  • 语言层面的异步处理解决方案
  • 基于promise实现,不能用于普通的回调函数

使用async/await 有着以下不同:

  • 函数前面多了一个async关键字,相比于generator函数前面加的是一个*
  • await只能用在async函数中,用在普通函数中会报错
  • 相比于generator,自带生成器,可以直接自动运行

关于async/await的优点,可以看Async/Await替代Promise的6个理由这篇文章,这篇文章详细列述了使用async/await的几大优点。

参考链接:

Javascript异步编程的4种方法

JavaScript 回调函数怎么理解

回调地狱的今生前世

深刻理解 ES 6 中的 Promise