JavaScript异步编程
简介
JavaScript是一种单线程执行的脚本语言,为了不让一段JavaScript代码执行时间过久,阻塞UI的渲染或者是鼠标事件处理,通常会采用一种异步的编程模式。这里就跟大家一起了解一下JavaScript的异步编程模式。
一、JavaScript的异步编程模式
1.1 为什么要异步编程
一开始就说过,JavaScript是一种单线程执行的脚本语言(这可能是由于历史原因或为了简单而采取的设计)。它的单线程表现在任何一个函数都要从头到尾执行完毕之后,才会执行另一个函数,界面的更新、鼠标事件的处理、计时器(setTimeout、setInterval等)的执行也需要先排队,后串行执行。假如有一段JavaScript从头到尾执行时间比较长,那么在执行期间任何UI更新都会被阻塞,界面事件处理也会停止响应。这种情况下就需要异步编程模式,目的就是把代码的运行打散或者让IO调用(例如AJAX)在后台运行,让界面更新和事件处理能够及时地运行。
下面是一个同步与异步执行的例子
01
<div
id="output"></div>
02
03
<button
onclick="updateSync
()">Run
Sync</button>
04
05
<button
onclick="updateAsync
()">Run
Async</button>
06
07
<script>
08
09
function updateSync()
{
10
for (var i
= 0; i < 1000; i++) {
11
document.getElementById('output').innerHTML
= i;
12
}
13
}
14
15
function updateAsync()
{
16
var i
= 0;
17
18
function updateLater()
{
19
document.getElementById('output').innerHTML
= (i++);
20
if (i
< 1000) {
21
setTimeout(updateLater,
0);
22
}
23
}
24
25
updateLater();
26
}
27
</script>
点击"Run Sync"按钮会调用updateSync的同步函数,逻辑非常简单,循环体内每次更新output结点的内容为i。如果在其他多线程模型下的语言,你可能会看到界面上以非常快的速度显示从0到999后停止。但是在JavaScript中,你会感觉按钮按下去的时候卡了一下,然后看到一个最终结果999,而没有中间过程,这就是因为在updateSync函数运行过程中UI更新被阻塞,只有当它结束退出后才会更新UI。如果你让这个函数的运行时间增加一下(例如把上限改为1 000 000),你会看到更明显的停顿,在停顿期间点击另一个按钮是没有任何反应的,只有结束之后才会处理另一个按钮的点击事件。
另一个按钮"Run Async"会调用updateAsync函数,它是一个异步函数,乍一看逻辑比较复杂,函数里先声明了一个局部变量i和嵌套函数updateLater(关于内嵌函数的介绍请看JavaScript世界的一等公民-函数),然后调用了updateLater,在这个函数中先是更新output结点的内容为i,然后通过setTimeout让updateLater函数异步执行。这个函数的运行后,你会看到UI界面上从0到999快速地更新过程,这就是异步执行的结果。
可见,在JavaScript中异步编程甚至是一种必要的编程模式。
1.2 异步编程的优缺点
异步编程的优点是显而易见的,异步编程你可以实现前面例子中一边运行一边更新的效果;或是利用异步IO让UI运行更加流畅,比如通过XMLHTTPRequest的异步接口获取网络数据,在获取完成后再更新界面,在异步获取数据的时候不会阻碍UI的更新。在众多HTML5设备API的设计中都充分采用了异步编程模式,例如W3C的File System API、File API、Indexed Database API,Windows 8 API,PhoneGap API,服务端脚本Node JS API等等。
异步编程也有一些缺点,造成深度嵌套的函数调用,破坏了原有的简单逻辑,让代码难以读懂。
二、异步编程接口设计
2.1 W3C原生接口
W3C原生接口的设计经常采用回调函数和事件触发形式,前者在调用异步函数时直接传入回调函数作为参数,后者在原始对象上绑定事件处理函数,异步函数出错时一般不会抛出异常,而是通过调用错误回调函数或触发错误事件。从语义上看,回调函数形式是为了获取某一个函数的运行结果,而事件触发形式通常会用于表示某些状态变化(加载、出错、进度变化、收到消息等等)。个人或团队开发小型项目时可以参考这两种形式的接口设计。
回调函数:例如W3C的File System API中,在请求虚拟文件系统实例、读写文件等接口中,都采用了回调函数的形式:
01
requestFileSystem(TEMPORARY,
1024 * 1024, function(fs)
{
02
03
//
异步获取虚拟文件系统实例fs
04
05
fs.root.getFile("already_there.txt", null, function (f)
{
06
07
//
获取文件already_there.txt
08
09
getAsText(f.file());
10
11
}, function(err)
{
12
13
//
获取文件出错
14
15
});
16
17
}, function(err)
{
18
19
//
获取虚拟文件系统失败
20
21
});
事件触发:例如W3C的XMLHTTPRequest(AJAX)就是一种通过事件触发这种形式实现,当AJAX请求成功或失败时触发onload、onerror事件:
01
var xhr
= new XMLHTTPRequest();
02
03
xhr.onload
= function()
{
04
05
//
加载成功时触发onload事件
06
07
};
08
09
xhr.onerror
= function()
{
10
11
//
加载失败时触发onerror事件
12
13
};
14
15
xhr.open(‘GET',
‘/get-ajax', true);
16
17
xhr.send(null);
2.2 第三方异步接口设计
采用回调函数形式的接口写代码,会带来比较严重的函数嵌套问题,就像著名的LISP一样,引入大量有争议性的括号,让本来是前后顺序执行的代码段形式上变成了一层套一层的结构,影响了JavaScript代码逻辑的清晰性。解决这个问题,要让逻辑上的先后顺序执行的代码,在形式上也是顺序的,而不是嵌套的,这就需要更好的异步接口设计方案。
CommonJS是一个著名的JavaScript的开源组织,目标是设计与JS环境无关的标准接口,并提供像Ruby、Python类似的标准库函数。在CommonJS中有三个异步编程模式相关的接口提案:Promises/A、Promises/B和Promises/D。Promise,中文意思为承诺,意思就是说承诺完成一个任务,在完成时告之是否执行成功,并返回结果。
这里我们只介绍最简单的异步接口Promises/A,在使用这种接口的函数时,函数的返回值是一个Promise对象,它有三种状态:不满足条件(unfulfilled)、满足条件(fulfilled)、失败(failed),顾名思义不满足条件状态就是异步函数刚刚调用,尚未真正执行时的状态,满足条件就是执行成功时的状态,失败就是执行失败的状态。它的接口函数也只有一个:
then(fulfilledHandler, errorHandler, progressHandler)
这三个参数分别是满足条件、失败以及进度有变化时的回调函数,他们的参数分别对应异步调用的结果,而then的返回值仍然是一个Promise对象,这个对象包含了上一步异步调用回调函数的返回值,因此可以链式地写下去,表现上成为顺序执行的逻辑。例如,假如W3C的File System API采用Promises/A的接口设计,2.1节的例子可以写作:
01
requestFileSystem(TEMPORARY,
1024 * 1024)
02
03
.then(function(fs)
{
04
05
 
补充:web前端 , JavaScript ,