简介javascript
JavaScript是一种单线程执行的脚本语言,为了避免让一段JavaScript代码执行时间太久,阻塞UI的渲染或者是鼠标事件处理,一般会采用一种异步的编程模式。这里就跟你们一块儿了解一下JavaScript的异步编程模式。html
1、JavaScript的异步编程模式java
1.1 为何要异步编程node
一 开始就说过,JavaScript是一种单线程执行的脚本语言(这多是因为历史缘由或为了简单而采起的设计)。它的单线程表如今任何一个函数都要从头到 尾执行完毕以后,才会执行另外一个函数,界面的更新、鼠标事件的处理、计时器(setTimeout、setInterval等)的执行也须要先排队,后串 行执行。假若有一段JavaScript从头至尾执行时间比较长,那么在执行期间任何UI更新都会被阻塞,界面事件处理也会中止响应。这种状况下就须要异 步编程模式,目的就是把代码的运行打散或者让IO调用(例如AJAX)在后台运行,让界面更新和事件处理可以及时地运行。git
下面是一个同步与异步执行的例子(在线测试连接http://jsfiddle.net/ghostoy/RPQgj/):github
01 |
<div id= "output" ></div> |
03 |
<button onclick= "updateSync ()" >Run Sync</button> |
05 |
<button onclick= "updateAsync ()" >Run Async</button> |
09 |
function updateSync() { |
10 |
for ( var i = 0; i < 1000; i++) { |
11 |
document.getElementById( 'output' ).innerHTML = i; |
15 |
function updateAsync() { |
18 |
function updateLater() { |
19 |
document.getElementById( 'output' ).innerHTML = (i++); |
21 |
setTimeout(updateLater, 0); |
点击"Run Sync"按钮会调用updateSync的同步函数,逻辑很是简单,循环体内每次更新output结点的内容为i。若是在其余多线程模型下的语言,你可 能会看到界面上以很是快的速度显示从0到999后中止。可是在JavaScript中,你会感受按钮按下去的时候卡了一下,而后看到一个最终结果999, 而没有中间过程,这就是由于在updateSync函数运行过程当中UI更新被阻塞,只有当它结束退出后才会更新UI。若是你让这个函数的运行时间增长一下 (例如把上限改成1 000 000),你会看到更明显的停顿,在停顿期间点击另外一个按钮是没有任何反应的,只有结束以后才会处理另外一个按钮的点击事件。ajax
另外一个按钮"Run Async"会调用updateAsync函数,它是一个异步函数,乍一看逻辑比较复杂,函数里先声明了一个局部变量i和嵌套函数updateLater(关于内嵌函数的介绍请看JavaScript世界的一等公民-函数),而后调用了updateLater,在这个函数中先是更新output结点的内容为i,而后经过setTimeout让updateLater函数异步执行。这个函数的运行后,你会看到UI界面上从0到999快速地更新过程,这就是异步执行的结果。编程
可见,在JavaScript中异步编程甚至是一种必要的编程模式。windows
1.2 异步编程的优缺点api
异 步编程的优势是显而易见的,异步编程你能够实现前面例子中一边运行一边更新的效果;或是利用异步IO让UI运行更加流畅,好比经过 XMLHTTPRequest的异步接口获取网络数据,在获取完成后再更新界面,在异步获取数据的时候不会阻碍UI的更新。在众多HTML5设备API的 设计中都充分采用了异步编程模式,例如W3C的File System API、File API、Indexed Database API,Windows 8 API,PhoneGap API,服务端脚本Node JS API等等。
异步编程也有一些缺点,形成深度嵌套的函数调用,破坏了原有的简单逻辑,让代码难以读懂。
2、异步编程接口设计
2.1 W3C原生接口
W3C原生接口的设计常常采用回调函数和事件触发形式,前者在调用异步函数时直接传入回调函数做为参数,后者在原始对象上绑定事件处理函数,异步函数出错时通常不会抛出异常,而是经过调用错误回调函数或触发错误事件。从语义上看,回调函数形式是为了获取某一个函数的运行结果,而事件触发形式一般会用于表示某些状态变化(加载、出错、进度变化、收到消息等等)。我的或团队开发小型项目时能够参考这两种形式的接口设计。
回调函数:例如W3C的File System API中,在请求虚拟文件系统实例、读写文件等接口中,都采用了回调函数的形式:
01 |
requestFileSystem(TEMPORARY, 1024 * 1024, function (fs) { |
05 |
fs.root.getFile( "already_there.txt" , null , function (f) { |
事件触发:例如W3C的XMLHTTPRequest(AJAX)就是一种经过事件触发这种形式实现,当AJAX请求成功或失败时触发onload、onerror事件:
01 |
var xhr = new XMLHTTPRequest(); |
03 |
xhr.onload = function () { |
09 |
xhr.onerror = function () { |
15 |
xhr.open(‘GET ', ‘/get-ajax' , true ); |
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) |
07 |
return fs.root.getFile( "already_there.txt" , null ); |
看是否是清楚多了?
实现Promises/A接口的JS库有不少,好比when.js、node-promise、promised-io等,微软的Windows 8 Metro应用的接口设计也采用了相同的接口设计,详见Asynchronious Programming in JavaScript with "Promises"。
2.3 异步同步化
第 三方的异步接口必定程度上解决了代码逻辑与执行顺序不一致的问题,可是仍然有些状况下,让代码难以读懂。咱们还以1.1节中的代码为 例,updateAsync即便采用Promises API并不会更好理解,而代码实现的功能其实就是一个很简单的循环+更新的功能。这时候就须要一些异步同步化来帮助实现。
所谓异步同步化顾名思义就是采用同步形式的语法实现异步调用。这里简单地介绍一下老赵的Jscex,它是一个纯JavaScript实现的库,能够在任何浏览器或JavaScript环境中运行,不只支持异步同步化的编程语法,还支持并行执行等特性。用Jscex来重写1.1节中的代码,将是这样(在线测试连接http://jsfiddle.net/ghostoy/ugxJJ/):
01 |
function updateAsync() { |
02 |
var update = eval(Jscex.compile( 'async' , function () { |
04 |
for ( var i = 0; i < 1000; i++) { |
05 |
document.getElementById( 'output' ).innerHTML = i; |
06 |
$await(Jscex.Async.sleep(0)); |
其中update是用Jscex编译生成的函数,它会返回一个Jscex的Task对象,经过调用它的start方法来执行这个Task。Update函 数的逻辑跟updateSync几乎同样,$await是Jscex增长的关键字,用于等待一个异步任务的调用结果,Jscex.Async.sleep 是Jscex内建的一个异步任务,用于显式地等待几毫秒,加入这行语句以后会被Jscex编译器生成异步的代码,实现一边计算一边更新UI的效果,代码结 构保持简洁清楚。
小结
JavaScript的异步编程模式不只是一种趋势,并且是一种必要,所以做为HTML5开发者是很是有必要掌握的。采用第三方的异步编程库和异步同步化的方法,会让代码结构相对简洁,便于维护,推荐开发人员掌握一二,提升团队开发效率。