- 浏览: 13764 次
- 性别:
- 来自: 上海
文章分类
最新评论
JavaScript异步编程
简介
JavaScript是一种单线程执行的脚本语言,为了不让一段JavaScript代码执行时间过久,阻塞UI的渲染或者是鼠标事件处理,通常会采用一种异步的编程模式。这里就跟大家一起了解一下JavaScript的异步编程模式。
一、JavaScript的异步编程模式
1.1 为什么要异步编程
一开始就说过,JavaScript是一种单线程执行的脚本语言(这可能是由于历史原因或为了简单而采取的设计)。它的单线程表现在任何一个函数都要从头到尾执行完毕之后,才会执行另一个函数,界面的更新、鼠标事件的处理、计时器(setTimeout、setInterval等)的执行也需要先排队,后串行执行。假如有一段JavaScript从头到尾执行时间比较长,那么在执行期间任何UI更新都会被阻塞,界面事件处理也会停止响应。这种情况下就需要异步编程模式,目的就是把代码的运行打散或者让IO调用(例如AJAX)在后台运行,让界面更新和事件处理能够及时地运行。
下面是一个同步与异步执行的例子(在线测试链接http://jsfiddle.net/ghostoy/RPQgj/):
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 // 异步获取虚拟文件系统实例fs
06
07 return fs.root.getFile("already_there.txt", null);
08
09 })
10
11 .then(function(f) {
12
13 // 获取文件already_there.txt
14
15 getAsText(f.file());
16
17 });
看是不是清楚多了?
实现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() {
03
04 for (var i = 0; i < 1000; i++) {
05 document.getElementById('output').innerHTML = i;
06 $await(Jscex.Async.sleep(0)); // sleep 0 ms to make it asynchronous
07 }
08
09 }));
10
11 update().start();
12 }
其中update是用Jscex编译生成的函数,它会返回一个Jscex的Task对象,通过调用它的start方法来执行这个Task。Update函数的逻辑跟updateSync几乎一样,$await是Jscex增加的关键字,用于等待一个异步任务的调用结果,Jscex.Async.sleep是Jscex内建的一个异步任务,用于显式地等待几毫秒,加入这行语句之后会被Jscex编译器生成异步的代码,实现一边计算一边更新UI的效果,代码结构保持简洁清楚。
小结
JavaScript的异步编程模式不仅是一种趋势,而且是一种必要,因此作为HTML5开发者是非常有必要掌握的。采用第三方的异步编程库和异步同步化的方法,会让代码结构相对简洁,便于维护,推荐开发人员掌握一二,提高团队开发效率。
///////////////////////////////////////////////////////////////////////////////
使用Jscex改善JavaScript异步编程体验
作者 赵劼 发布于 2011年7月27日
领域
语言 & 开发
主题
Ruby ,
JavaScript ,
Java ,
动态语言 ,
《架构师》月刊 ,
.NET ,
电子杂志 ,
Node.js ,
Jscex
新浪微博 腾讯微博 豆瓣网 Twitter Facebook linkedin 邮件分享 更多
JavaScript是互联网时代编程语言的霸主,统领浏览器至今已有许多年头,而这股风潮很可能随着HTML 5的兴起而愈演愈烈。如今JavaScript更是在Node.js的帮助下进军服务器编程领域。“单线程”和“无阻塞”是JavaScript的天性,因此任何需要“耗时”的操作,例如等待、网络通信、磁盘IO都只能提供“异步”的编程接口。尽管这对服务器的伸缩性和客户端的响应能力都大有脾益,但是异步接口在使用上要比传统的线性编程困难许多,因此也诞生了如jQuery Deferred这样的辅助类库。Jscex的主要目的也是简化异步编程,但它使用了一种与传统辅助类库截然不同的方式,尽可能地将异步编程体验带领到新的高度。
相关厂商内容
《JavaScript语言精粹》作者Douglas Crockford确认参会
GitHub研发团队成员Corey Donoho QCon分享Github架构设计与团队合作
Google商用Apps创始人Derek Parham确认参加QCon北京2013
百度技术沙龙第三十三期:推荐系统实战(12月22日 周六)
相关赞助商
QCon北京2013,Node专场:NodeJS如何在大企业应用落地发挥成效,详情请点击!
JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作。这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码逻辑,但是大量异步操作所带来的回调函数,会把我们的算法分解地支离破碎。此时我们不能用if来实现逻辑分支,也不能用while/for/do来实现循环,更不用提异步操作之间的组合、错误处理以及取消操作了。
快速入门:排序动画
我们先来看一个简单的例子。“冒泡排序”是最常见的排序算法之一,它的JavaScript实现如下:
var compare = function (x, y) {
return x - y;
}
var swap = function (array, i, j) {
var t = array[i];
array[i] = array[j];
array[j] = t;
}
var bubbleSort = function (array) {
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array.length - i; j++) {
if (compare(array[j], array[j + 1]) > 0) {
swap(array, j, j + 1);
}
}
}
}
由于某些原因——例如教学所需,我们希望能够通过动画来直观地感受不同排序算法之间的差异。将一个排序算法改写为动画效果的“基本策略”十分简单:
在每次元素“交换”和“比较”操作时暂停一小会儿(因为它们是排序算法的主要耗时所在)。
在元素“交换”过后重绘图像。
只需增加这样两个“简单”的功能,便可以形成算法的动画效果。但实际上,实现这个策略并没有听上去那么容易。在其它许多语言或是运行环境中,我们可以使用sleep方法来暂停当前线程。这对代码的逻辑结构的影响极小。但是在JavaScript中,我们只有setTimeout可以做到“延迟”执行某个操作。setTimeout需要与回调函数配合使用,但这会严重破坏算法的逻辑结构,例如,我们再也无法使用for来实现哪怕是最最简单的循环操作了。因此,排序算法的动画似乎只能这么写:
// 异步操作签名约定:
// function (arg1, arg2, ..., callback) {
// 异步操作完成后使用callback回传结果
// }
var compareAsync = function (x, y, callback) {
// 延迟10毫秒返回结果
setTimeout(10, function () {
callback(x - y);
});
}
var swapAsync = function (a, i, j, callback) {
// 交换元素
var t = a[i]; a[i] = a[j]; a[j] = t;
// 重绘
repaint(a);
// 延迟20毫秒才返回
setTimeout(20, callback);
}
// 外部循环,从下标为i的元素开始处理
var outerLoopAsync = function (array, i, callback) {
// 如果i还在数组长度范围内
if (i < array.length) {
// 则进入内部循环,与下标为i的元素进行比较
innerLoopAsync(array, i, 0, function () {
// 内部循环结束以后,在外部循环中处理i的下一个元素
outerLoopAsync(array, i + 1, callback);
});
} else {
// i超出数组长度,表示外层循环结束
callback();
}
}
// 内部循环,从下标j开始,与下标为i的元素进行比较
var innerLoopAsync = function (array, i, j, callback) {
// 如果j在合适范围内
if (j < array.length - i) {
// 则比较下标j及其相邻元素
compareAsync(array[j], array[j + 1], function (r) {
// 如果次序不对
if (r > 0) {
// 则交换及其相邻元素
swapAsync(array, j, j + 1, function () {
// 交换之后,则重复内层循环比较下标j的下一个元a素
innerLoopAsync(array, i, j + 1, callback);
});
} else {
// 假如次序已经正确·,则直接重复内存循环比较下标j的下一个元a素
innerLoopAsync(array, i, j + 1, callback);
}
});
} else {
// j已经超出范围,一个元素已经处于合适的位置,内层循环结束
callback();
}
}
// 冒泡排序主方法
var bubbleSortAsync = function (array, callback) {
// 从第一个元素开始执行外部循环,
// 外部循环结束则意味着排序完毕
outerLoop(array, 0, callback || function () { });
}
// 调用
var array = ...; // 初始化数组
bubbleSortAsync(array);
相信您也可以看得出来,如果使用传统回调的方式来实现一个冒泡排序动画会有多么麻烦。而“支离破碎”所导致的更严重的问题,是代码“语义”方面的损失。例如,新来一位开发人员想要维护这段代码,他能够看出上面这段代码是“冒泡排序”吗?如果您给出“冒泡排序”的动画,又能轻易地将算法“说明”给别人理解吗?如果需要简单补充一些功能,又该将新代码添加在何处?使用传统线性编程的优势之一,在于容易快速编写出逻辑清晰而“内聚”的实现,即使需要补充一些功能,则可以通过局部变量将状态修改控制至极小。我们几乎可以这么说,基于回调函数的异步编程,让许多传统程序设计中总结出来的实践与模式付诸东流。
不过有了Jscex以后世界便大不一样了,它将编程体验变得“如初见般美好”:
// 异步的比较操作
var compareAsync = eval(Jscex.compile("async", function (x, y) {
$await(Jscex.Async.sleep(10)); // 等待10毫秒
return x - y;
}));
// 异步的交换操作
var swapAsync = eval(Jscex.compile("async", function (array, i, j) {
var t = array[i];
array[i] = array[j];
array[j] = t;
repaint(array); // 重绘
$await(Jscex.Async.sleep(20)); // 等待20毫秒
}));
// 异步的冒泡排序
var bubbleSortAsync = eval(Jscex.compile("async", function (array) {
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array.length - i; j++) {
// 执行异步的比较操作
var r = $await(compareAsync(array[j], array[j + 1]));
if (r > 0) {
// 执行异步的交换操作
$await(swapAsync(array, j, j + 1));
}
}
}
}));
// 调用
var array = ...; // 初始化数组
bubbleSortAsync(array).start();
以上这段代码几乎不用做任何解释,因为它完全便是在标准的“冒泡排序”算法之上,增加了之前所提到的“基本策略”。这便是Jscex改进异步编程体验的手段:程序员编写最自然的代码,并使用$await来执行其中的异步操作。Jscex提供的编译器(即compile方法)会将一个普通的JavaScript函数编译为“回调函数”组织起来的异步实现,做到“线性编码,异步执行”的效果。
您可以在此观察冒泡排序的动画效果(需要IE9,Chrome,Firefox等支持Canvas的浏览器)。这张页面里还实现了选择排序和快速排序算法的动画,都是基于Jscex的优雅实现。如果您感兴趣,也可以使用传统的、基于回调的方式来编写这些算法动画,然后跟页面中的代码实现进行对比,便可以更好地了解Jscex的优势。
使用Jscex开发异步程序
Jscex可以在任何支持JavaScript(ECMAScript 3)的运行环境里执行,例如,包括IE 6在内的现代浏览器,服务器端的Node.js,以及如Rhino一样的JavaScript引擎等等,它们的区别仅仅在于“引入Jscex脚本文件”的方式不同而已。Jscex模块化十分细致,在使用时需要引入不少文件,部分原因也是由于JavaScript环境至今还缺少一个包管理机制所造成的:
lib/json2.js:由Douglas Crockfod编写的JSON生成器,对于原生不支持JSON.stringify方法的JavaScript环境(例如早期版本的IE),则需要引入该文件。
lib/uglifyjs-parser.js:UglifyJS项目(jQuery项目官方使用的压缩工具)所使用的的JavaScript解析器,这是LISP项目parse-js的 JavaScript 移植,它负责Jscex中的语法解析工作。
src/jscex.js:JIT编译器实现,负责在运行时生成代码。这也是Jscex.compile方法的具体实现所在。
以上三个文件构成了Jscex的编译器核心,它们只需在开发环境中使用(例如在页面引用它们),目的只是为了提供近乎原生JavaScript的开发体验。对于Jscex来说,它的首要原则(没有之一)便是“保证JavaScript程序员的传统开发体验”。而对于开发和生产环境都必不可少的只有以下两个文件:
src/jscex.builderBase.js:Jscex中“构造器”的公用部分。
src/jscex.async.js:Jscex的“异步构造器”,用于支持异步程序开发。
这两个文件在精简和gzip之后,只有3KB左右大小,几乎不会给应用程序带来什么影响。
如果您要编写一个Jscex异步函数,则只需要将一个普通的函数定义放到一段“架子”代码中即可:
// 普通函数
var giveMeFive = function (arg0, arg1, ..., argN) {
// 实现
return 5;
};
// Jscex异步函数
var giveMeFiveAsync = eval(Jscex.compile("async", function (arg0, arg1, ..., argN) {
// 实现
return 5;
}));
Jscex.compile方法会根据它获得的“构造器名称(即async)”和“函数对象”生成其对应的“新函数”的代码,而这段代码会立即被eval执行。这段“架子代码”看上去略显冗余,如果您觉得输入麻烦也可以将其保存为编辑器的“代码片段(Code Snippet)”,因为它在Jscex使用过程中几乎不会有任何变化,我们也无需过于关注其含义。
“架子代码”的另一个作用是“区分”普通函数和异步函数。例如上面的代码中,giveMeFive会返回5,但giveMeFiveAsync在执行后返回的其实是一个“将会返回5”的Future对象——在Jscex中我们将其称为“任务”。除非我们通过start方法启动这个任务(Jscex异步函数中使用$await操作在需要时会调用start方法),则函数里的代码永远不会执行。因此,普通函数和异步函数在功能、含义和表现上都有不同,而通过“架子代码”的便能很方便地把它们区分开来。
在一个Jscex异步函数中,我们用$await操作来表示“等待任务返回结果(或出错),如果它还未执行,则同时启动这个任务”。$await的参数是一个Jscex任务对象,我们可以把任意的异步操作轻松地封装为一个Jscex任务。例如在Jscex的异步类库中就内置了Jscex.Async.sleep函数,它封装了setTimeout函数。显然,执行任何一个Jscex异步函数,您都可以得到这样一个标准的异步任务对象。
除了在Jscex异步函数中通过$await来操作之外,我们也可以手动调用任务的start方法来启动一个任务。Jscex异步任务模型虽然简单,但它是Jscex异步编程的基石,它让“编译器”的核心功能变得小巧、简单和紧凑,许多功能以及使用模式都能在“类库”层面扩展出来。在今后的文章中,我们也会了解如何将一个异步操作封装为Jscex任务,以及围绕这个任务模型进行开发和扩展。
平易近人的编译器和eval
从我之前的经验来看,一些朋友可能会被“编译器”的字样吓到,认为Jscex是一个“重型”的解决方案。还有一些朋友在脑海里深深印有“eval很邪恶”的印象,于是同样望而却步。其实这些都是对Jscex的误解,这里我打算着重解释一下这方面的问题。
如今“编译器”其实并不是什么特别神秘的东西,事实上可能您早就在使用针对JavaScript的编译器了。例如,Google的Closure Compiler便是这样一个东西。Closure Compiler会接受一段JavaScript代码,并输出其“等价”并“精简”后的代码。Closure Compiler的作用是“减小文件体积”,而Jscex的作用便是将一个JavaScript函数转化成一个新的函数,以符合某些场景(如异步编程)的需要而已。另一方面,Jscex的转换操作也涉及代码解析,语法树的优化以及新代码的输出,因此无论从功能还是从实现角度来说,Jscex的核心都是一个标准的“编译器”。
传统的编译器往往会给开发人员在代码执行之前增加一个额外步骤(编译),这对编程体验是一种损害。JavaScript程序员往往习惯于“修改后刷新页面”便能立即看到结果,但是如某些将C#或Java语言转化为JavaScript的解决方案,往往都需要开发人员在“刷新页面”之前重新生成一遍JavaScript代码。Jscex则不然,正如之前提到的那样,Jscex的首要原则是“尽可能保证JavaScript程序员的传统开发体验”。Jscex编译器的一大特色,便是“在运行时生成代码”。Jscex只是JavaScript开发中所使用的类库,它几乎不会对“JavaScript编程”本身有任何改变。换句话说,开发人员编写的就是JavaScript代码,它的载体就是普通的JavaScript文件,文件加载也好,代码执行行为也罢,都和普通的JavaScript开发一样。当您修改了Jscex异步函数的实现之后,Jscex.compile方法在代码执行时自然会生成新的函数代码,因此并不会给开发人员增加任何额外负担。
Jscex.compile生成的代码会由eval执行,有朋友会认为这么做会影响性能或是安全性。但事实上,无论是eval还是Jscex.compile,都只是为了保证开发过程中的体验(修改后立即生效)。真正在生产环境里执行的代码,是不会出现eval和Jscex.compile的,因为Jscex还提供了一个AOT编译器(相对于在运行时生成代码的JIT编译器而言)。
AOT编译器也是一段JavaScript代码,使用Node.js执行。使用方法为:
node scripts/jscexc.js --input input_file --output output_file
AOT编译器会静态分析输入的脚本文件,找出其中的eval与Jscex.compile函数调用,直接将“动态编译”的结果写入eval处。例如compareAsync的原始代码:
var compareAsync = eval(Jscex.compile("async", function (x, y) {
$await(Jscex.Async.sleep(10));
return x - y;
}));
编译后的代码便会成为如下形式,目前您无需理解这段代码的含义。Jscex对最终编译输出的代码经过精心设计,尽可能地让其保留可读性及可调式性,这点在今后的文章中也会加以说明和演示。
var compareAsync = (function (x, y) {
var $_b = Jscex.builders["async"];
return $_b.Start(this,
$_b.Delay(function () {
return $_b.Bind(Jscex.Async.sleep(10), function () {
return $_b.Return(x - y);
});
})
);
});
原始代码在经过AOT编译之后,不仅在运行性能方面有所提高(节省了编译和动态执行的开销,并可以在ECMAScript 5的Strict Mode下运行),还能让代码摆脱Jscex编译器执行。在排除了编译器代码之后,Jscex的异步类库在精简和压缩后只有3KB左右大小,十分适合互联网产品使用。
总结
异步编程的困难有目共睹,因此为了辅助异步程序开发出现过许多尝试。在JavaScript编程领域,大部分解决方案都是设法通过提供一些API来更好地组织错综复杂的回调函数,但Jscex走的是另外一条道路。Jscex的目的,是希望尽可能保留JavaScript程序员原有的编程习惯及逻辑组织方式,让“编译器”来生成那些包含了回调函数的代码。类似的功能已经在F#和Scala中获得了成功,也即将在下个版本的C#里出现,而Jscex则是将其引入至JavaScript编程中。
Jscex基于BSD授权协议开源,代码库放置在GitHub上,并同步至SNDA Code。
简介
JavaScript是一种单线程执行的脚本语言,为了不让一段JavaScript代码执行时间过久,阻塞UI的渲染或者是鼠标事件处理,通常会采用一种异步的编程模式。这里就跟大家一起了解一下JavaScript的异步编程模式。
一、JavaScript的异步编程模式
1.1 为什么要异步编程
一开始就说过,JavaScript是一种单线程执行的脚本语言(这可能是由于历史原因或为了简单而采取的设计)。它的单线程表现在任何一个函数都要从头到尾执行完毕之后,才会执行另一个函数,界面的更新、鼠标事件的处理、计时器(setTimeout、setInterval等)的执行也需要先排队,后串行执行。假如有一段JavaScript从头到尾执行时间比较长,那么在执行期间任何UI更新都会被阻塞,界面事件处理也会停止响应。这种情况下就需要异步编程模式,目的就是把代码的运行打散或者让IO调用(例如AJAX)在后台运行,让界面更新和事件处理能够及时地运行。
下面是一个同步与异步执行的例子(在线测试链接http://jsfiddle.net/ghostoy/RPQgj/):
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 // 异步获取虚拟文件系统实例fs
06
07 return fs.root.getFile("already_there.txt", null);
08
09 })
10
11 .then(function(f) {
12
13 // 获取文件already_there.txt
14
15 getAsText(f.file());
16
17 });
看是不是清楚多了?
实现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() {
03
04 for (var i = 0; i < 1000; i++) {
05 document.getElementById('output').innerHTML = i;
06 $await(Jscex.Async.sleep(0)); // sleep 0 ms to make it asynchronous
07 }
08
09 }));
10
11 update().start();
12 }
其中update是用Jscex编译生成的函数,它会返回一个Jscex的Task对象,通过调用它的start方法来执行这个Task。Update函数的逻辑跟updateSync几乎一样,$await是Jscex增加的关键字,用于等待一个异步任务的调用结果,Jscex.Async.sleep是Jscex内建的一个异步任务,用于显式地等待几毫秒,加入这行语句之后会被Jscex编译器生成异步的代码,实现一边计算一边更新UI的效果,代码结构保持简洁清楚。
小结
JavaScript的异步编程模式不仅是一种趋势,而且是一种必要,因此作为HTML5开发者是非常有必要掌握的。采用第三方的异步编程库和异步同步化的方法,会让代码结构相对简洁,便于维护,推荐开发人员掌握一二,提高团队开发效率。
///////////////////////////////////////////////////////////////////////////////
使用Jscex改善JavaScript异步编程体验
作者 赵劼 发布于 2011年7月27日
领域
语言 & 开发
主题
Ruby ,
JavaScript ,
Java ,
动态语言 ,
《架构师》月刊 ,
.NET ,
电子杂志 ,
Node.js ,
Jscex
新浪微博 腾讯微博 豆瓣网 Twitter Facebook linkedin 邮件分享 更多
JavaScript是互联网时代编程语言的霸主,统领浏览器至今已有许多年头,而这股风潮很可能随着HTML 5的兴起而愈演愈烈。如今JavaScript更是在Node.js的帮助下进军服务器编程领域。“单线程”和“无阻塞”是JavaScript的天性,因此任何需要“耗时”的操作,例如等待、网络通信、磁盘IO都只能提供“异步”的编程接口。尽管这对服务器的伸缩性和客户端的响应能力都大有脾益,但是异步接口在使用上要比传统的线性编程困难许多,因此也诞生了如jQuery Deferred这样的辅助类库。Jscex的主要目的也是简化异步编程,但它使用了一种与传统辅助类库截然不同的方式,尽可能地将异步编程体验带领到新的高度。
相关厂商内容
《JavaScript语言精粹》作者Douglas Crockford确认参会
GitHub研发团队成员Corey Donoho QCon分享Github架构设计与团队合作
Google商用Apps创始人Derek Parham确认参加QCon北京2013
百度技术沙龙第三十三期:推荐系统实战(12月22日 周六)
相关赞助商
QCon北京2013,Node专场:NodeJS如何在大企业应用落地发挥成效,详情请点击!
JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作。这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码逻辑,但是大量异步操作所带来的回调函数,会把我们的算法分解地支离破碎。此时我们不能用if来实现逻辑分支,也不能用while/for/do来实现循环,更不用提异步操作之间的组合、错误处理以及取消操作了。
快速入门:排序动画
我们先来看一个简单的例子。“冒泡排序”是最常见的排序算法之一,它的JavaScript实现如下:
var compare = function (x, y) {
return x - y;
}
var swap = function (array, i, j) {
var t = array[i];
array[i] = array[j];
array[j] = t;
}
var bubbleSort = function (array) {
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array.length - i; j++) {
if (compare(array[j], array[j + 1]) > 0) {
swap(array, j, j + 1);
}
}
}
}
由于某些原因——例如教学所需,我们希望能够通过动画来直观地感受不同排序算法之间的差异。将一个排序算法改写为动画效果的“基本策略”十分简单:
在每次元素“交换”和“比较”操作时暂停一小会儿(因为它们是排序算法的主要耗时所在)。
在元素“交换”过后重绘图像。
只需增加这样两个“简单”的功能,便可以形成算法的动画效果。但实际上,实现这个策略并没有听上去那么容易。在其它许多语言或是运行环境中,我们可以使用sleep方法来暂停当前线程。这对代码的逻辑结构的影响极小。但是在JavaScript中,我们只有setTimeout可以做到“延迟”执行某个操作。setTimeout需要与回调函数配合使用,但这会严重破坏算法的逻辑结构,例如,我们再也无法使用for来实现哪怕是最最简单的循环操作了。因此,排序算法的动画似乎只能这么写:
// 异步操作签名约定:
// function (arg1, arg2, ..., callback) {
// 异步操作完成后使用callback回传结果
// }
var compareAsync = function (x, y, callback) {
// 延迟10毫秒返回结果
setTimeout(10, function () {
callback(x - y);
});
}
var swapAsync = function (a, i, j, callback) {
// 交换元素
var t = a[i]; a[i] = a[j]; a[j] = t;
// 重绘
repaint(a);
// 延迟20毫秒才返回
setTimeout(20, callback);
}
// 外部循环,从下标为i的元素开始处理
var outerLoopAsync = function (array, i, callback) {
// 如果i还在数组长度范围内
if (i < array.length) {
// 则进入内部循环,与下标为i的元素进行比较
innerLoopAsync(array, i, 0, function () {
// 内部循环结束以后,在外部循环中处理i的下一个元素
outerLoopAsync(array, i + 1, callback);
});
} else {
// i超出数组长度,表示外层循环结束
callback();
}
}
// 内部循环,从下标j开始,与下标为i的元素进行比较
var innerLoopAsync = function (array, i, j, callback) {
// 如果j在合适范围内
if (j < array.length - i) {
// 则比较下标j及其相邻元素
compareAsync(array[j], array[j + 1], function (r) {
// 如果次序不对
if (r > 0) {
// 则交换及其相邻元素
swapAsync(array, j, j + 1, function () {
// 交换之后,则重复内层循环比较下标j的下一个元a素
innerLoopAsync(array, i, j + 1, callback);
});
} else {
// 假如次序已经正确·,则直接重复内存循环比较下标j的下一个元a素
innerLoopAsync(array, i, j + 1, callback);
}
});
} else {
// j已经超出范围,一个元素已经处于合适的位置,内层循环结束
callback();
}
}
// 冒泡排序主方法
var bubbleSortAsync = function (array, callback) {
// 从第一个元素开始执行外部循环,
// 外部循环结束则意味着排序完毕
outerLoop(array, 0, callback || function () { });
}
// 调用
var array = ...; // 初始化数组
bubbleSortAsync(array);
相信您也可以看得出来,如果使用传统回调的方式来实现一个冒泡排序动画会有多么麻烦。而“支离破碎”所导致的更严重的问题,是代码“语义”方面的损失。例如,新来一位开发人员想要维护这段代码,他能够看出上面这段代码是“冒泡排序”吗?如果您给出“冒泡排序”的动画,又能轻易地将算法“说明”给别人理解吗?如果需要简单补充一些功能,又该将新代码添加在何处?使用传统线性编程的优势之一,在于容易快速编写出逻辑清晰而“内聚”的实现,即使需要补充一些功能,则可以通过局部变量将状态修改控制至极小。我们几乎可以这么说,基于回调函数的异步编程,让许多传统程序设计中总结出来的实践与模式付诸东流。
不过有了Jscex以后世界便大不一样了,它将编程体验变得“如初见般美好”:
// 异步的比较操作
var compareAsync = eval(Jscex.compile("async", function (x, y) {
$await(Jscex.Async.sleep(10)); // 等待10毫秒
return x - y;
}));
// 异步的交换操作
var swapAsync = eval(Jscex.compile("async", function (array, i, j) {
var t = array[i];
array[i] = array[j];
array[j] = t;
repaint(array); // 重绘
$await(Jscex.Async.sleep(20)); // 等待20毫秒
}));
// 异步的冒泡排序
var bubbleSortAsync = eval(Jscex.compile("async", function (array) {
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array.length - i; j++) {
// 执行异步的比较操作
var r = $await(compareAsync(array[j], array[j + 1]));
if (r > 0) {
// 执行异步的交换操作
$await(swapAsync(array, j, j + 1));
}
}
}
}));
// 调用
var array = ...; // 初始化数组
bubbleSortAsync(array).start();
以上这段代码几乎不用做任何解释,因为它完全便是在标准的“冒泡排序”算法之上,增加了之前所提到的“基本策略”。这便是Jscex改进异步编程体验的手段:程序员编写最自然的代码,并使用$await来执行其中的异步操作。Jscex提供的编译器(即compile方法)会将一个普通的JavaScript函数编译为“回调函数”组织起来的异步实现,做到“线性编码,异步执行”的效果。
您可以在此观察冒泡排序的动画效果(需要IE9,Chrome,Firefox等支持Canvas的浏览器)。这张页面里还实现了选择排序和快速排序算法的动画,都是基于Jscex的优雅实现。如果您感兴趣,也可以使用传统的、基于回调的方式来编写这些算法动画,然后跟页面中的代码实现进行对比,便可以更好地了解Jscex的优势。
使用Jscex开发异步程序
Jscex可以在任何支持JavaScript(ECMAScript 3)的运行环境里执行,例如,包括IE 6在内的现代浏览器,服务器端的Node.js,以及如Rhino一样的JavaScript引擎等等,它们的区别仅仅在于“引入Jscex脚本文件”的方式不同而已。Jscex模块化十分细致,在使用时需要引入不少文件,部分原因也是由于JavaScript环境至今还缺少一个包管理机制所造成的:
lib/json2.js:由Douglas Crockfod编写的JSON生成器,对于原生不支持JSON.stringify方法的JavaScript环境(例如早期版本的IE),则需要引入该文件。
lib/uglifyjs-parser.js:UglifyJS项目(jQuery项目官方使用的压缩工具)所使用的的JavaScript解析器,这是LISP项目parse-js的 JavaScript 移植,它负责Jscex中的语法解析工作。
src/jscex.js:JIT编译器实现,负责在运行时生成代码。这也是Jscex.compile方法的具体实现所在。
以上三个文件构成了Jscex的编译器核心,它们只需在开发环境中使用(例如在页面引用它们),目的只是为了提供近乎原生JavaScript的开发体验。对于Jscex来说,它的首要原则(没有之一)便是“保证JavaScript程序员的传统开发体验”。而对于开发和生产环境都必不可少的只有以下两个文件:
src/jscex.builderBase.js:Jscex中“构造器”的公用部分。
src/jscex.async.js:Jscex的“异步构造器”,用于支持异步程序开发。
这两个文件在精简和gzip之后,只有3KB左右大小,几乎不会给应用程序带来什么影响。
如果您要编写一个Jscex异步函数,则只需要将一个普通的函数定义放到一段“架子”代码中即可:
// 普通函数
var giveMeFive = function (arg0, arg1, ..., argN) {
// 实现
return 5;
};
// Jscex异步函数
var giveMeFiveAsync = eval(Jscex.compile("async", function (arg0, arg1, ..., argN) {
// 实现
return 5;
}));
Jscex.compile方法会根据它获得的“构造器名称(即async)”和“函数对象”生成其对应的“新函数”的代码,而这段代码会立即被eval执行。这段“架子代码”看上去略显冗余,如果您觉得输入麻烦也可以将其保存为编辑器的“代码片段(Code Snippet)”,因为它在Jscex使用过程中几乎不会有任何变化,我们也无需过于关注其含义。
“架子代码”的另一个作用是“区分”普通函数和异步函数。例如上面的代码中,giveMeFive会返回5,但giveMeFiveAsync在执行后返回的其实是一个“将会返回5”的Future对象——在Jscex中我们将其称为“任务”。除非我们通过start方法启动这个任务(Jscex异步函数中使用$await操作在需要时会调用start方法),则函数里的代码永远不会执行。因此,普通函数和异步函数在功能、含义和表现上都有不同,而通过“架子代码”的便能很方便地把它们区分开来。
在一个Jscex异步函数中,我们用$await操作来表示“等待任务返回结果(或出错),如果它还未执行,则同时启动这个任务”。$await的参数是一个Jscex任务对象,我们可以把任意的异步操作轻松地封装为一个Jscex任务。例如在Jscex的异步类库中就内置了Jscex.Async.sleep函数,它封装了setTimeout函数。显然,执行任何一个Jscex异步函数,您都可以得到这样一个标准的异步任务对象。
除了在Jscex异步函数中通过$await来操作之外,我们也可以手动调用任务的start方法来启动一个任务。Jscex异步任务模型虽然简单,但它是Jscex异步编程的基石,它让“编译器”的核心功能变得小巧、简单和紧凑,许多功能以及使用模式都能在“类库”层面扩展出来。在今后的文章中,我们也会了解如何将一个异步操作封装为Jscex任务,以及围绕这个任务模型进行开发和扩展。
平易近人的编译器和eval
从我之前的经验来看,一些朋友可能会被“编译器”的字样吓到,认为Jscex是一个“重型”的解决方案。还有一些朋友在脑海里深深印有“eval很邪恶”的印象,于是同样望而却步。其实这些都是对Jscex的误解,这里我打算着重解释一下这方面的问题。
如今“编译器”其实并不是什么特别神秘的东西,事实上可能您早就在使用针对JavaScript的编译器了。例如,Google的Closure Compiler便是这样一个东西。Closure Compiler会接受一段JavaScript代码,并输出其“等价”并“精简”后的代码。Closure Compiler的作用是“减小文件体积”,而Jscex的作用便是将一个JavaScript函数转化成一个新的函数,以符合某些场景(如异步编程)的需要而已。另一方面,Jscex的转换操作也涉及代码解析,语法树的优化以及新代码的输出,因此无论从功能还是从实现角度来说,Jscex的核心都是一个标准的“编译器”。
传统的编译器往往会给开发人员在代码执行之前增加一个额外步骤(编译),这对编程体验是一种损害。JavaScript程序员往往习惯于“修改后刷新页面”便能立即看到结果,但是如某些将C#或Java语言转化为JavaScript的解决方案,往往都需要开发人员在“刷新页面”之前重新生成一遍JavaScript代码。Jscex则不然,正如之前提到的那样,Jscex的首要原则是“尽可能保证JavaScript程序员的传统开发体验”。Jscex编译器的一大特色,便是“在运行时生成代码”。Jscex只是JavaScript开发中所使用的类库,它几乎不会对“JavaScript编程”本身有任何改变。换句话说,开发人员编写的就是JavaScript代码,它的载体就是普通的JavaScript文件,文件加载也好,代码执行行为也罢,都和普通的JavaScript开发一样。当您修改了Jscex异步函数的实现之后,Jscex.compile方法在代码执行时自然会生成新的函数代码,因此并不会给开发人员增加任何额外负担。
Jscex.compile生成的代码会由eval执行,有朋友会认为这么做会影响性能或是安全性。但事实上,无论是eval还是Jscex.compile,都只是为了保证开发过程中的体验(修改后立即生效)。真正在生产环境里执行的代码,是不会出现eval和Jscex.compile的,因为Jscex还提供了一个AOT编译器(相对于在运行时生成代码的JIT编译器而言)。
AOT编译器也是一段JavaScript代码,使用Node.js执行。使用方法为:
node scripts/jscexc.js --input input_file --output output_file
AOT编译器会静态分析输入的脚本文件,找出其中的eval与Jscex.compile函数调用,直接将“动态编译”的结果写入eval处。例如compareAsync的原始代码:
var compareAsync = eval(Jscex.compile("async", function (x, y) {
$await(Jscex.Async.sleep(10));
return x - y;
}));
编译后的代码便会成为如下形式,目前您无需理解这段代码的含义。Jscex对最终编译输出的代码经过精心设计,尽可能地让其保留可读性及可调式性,这点在今后的文章中也会加以说明和演示。
var compareAsync = (function (x, y) {
var $_b = Jscex.builders["async"];
return $_b.Start(this,
$_b.Delay(function () {
return $_b.Bind(Jscex.Async.sleep(10), function () {
return $_b.Return(x - y);
});
})
);
});
原始代码在经过AOT编译之后,不仅在运行性能方面有所提高(节省了编译和动态执行的开销,并可以在ECMAScript 5的Strict Mode下运行),还能让代码摆脱Jscex编译器执行。在排除了编译器代码之后,Jscex的异步类库在精简和压缩后只有3KB左右大小,十分适合互联网产品使用。
总结
异步编程的困难有目共睹,因此为了辅助异步程序开发出现过许多尝试。在JavaScript编程领域,大部分解决方案都是设法通过提供一些API来更好地组织错综复杂的回调函数,但Jscex走的是另外一条道路。Jscex的目的,是希望尽可能保留JavaScript程序员原有的编程习惯及逻辑组织方式,让“编译器”来生成那些包含了回调函数的代码。类似的功能已经在F#和Scala中获得了成功,也即将在下个版本的C#里出现,而Jscex则是将其引入至JavaScript编程中。
Jscex基于BSD授权协议开源,代码库放置在GitHub上,并同步至SNDA Code。
发表评论
-
判断浏览器类型及版本号
2012-12-12 17:51 414navigator.appName if("Net ... -
JavaScript 图片滑动切换效果
2012-12-12 14:14 460序一(08/07/06) 看到alibaba的一个图片切换效 ... -
jquery 如何实现全选 不选 反选
2012-12-12 14:00 500<script src="jquery-1.4 ... -
windows下Sublime Text 2
2012-12-11 11:00 5051 下载 Sublime Text 2 http://www. ... -
很郁闷 window.location.href 竟然没有权限
2012-12-10 16:16 1487很郁闷 window.location.href 竟然没有权限 ... -
ie6,7 location.href 没有权限的出现原因及解决方案
2012-12-10 10:04 665ie6,7 location.href 没有权限的出现原因及解 ...
相关推荐
Jscex是JavaScript异步流程控制类库,旨在解决JavaScript异步编程的难题。JavaScript是一门单线程语言,用户代码一旦发生阻塞则整个程序便会陷入停滞,因此它天生带有很强的异步特性。异步编程其实十分困难,因为...
Jscex类库是一款专为JavaScript异步编程设计的库,它主要的目的是为了简化复杂的回调函数嵌套,提升代码的可读性和可维护性。无论是应用于浏览器环境还是服务器端(如Node.js),Jscex都能提供一致的编程体验。 在...
Jscex是一个JavaScript的类库,它主要设计用于在浏览器端和Node.js环境中提供一种静态类型检查和编译的功能,从而提升代码的质量和可维护性。这个类库源自2011年,当时的版本可能包含了早期对JavaScript语言特性的...
var drawAsync = eval&...Jscex.compile("async", function () { while (true) { that.context.drawImage(that.offCanvas, 0, 0,that.canvas.width,that.canvas.height); $await(Jscex.Async.sleep(50)); } }))
Jscex,全称为JavaScript Expression,是一款轻量级的JavaScript编译器,它允许开发者使用类似CoffeeScript的语法来编写JavaScript代码,提高代码的可读性和可维护性。Jscex的核心功能是将这种简洁的语法转换为标准...
标题中提到的“ITCYC”很可能是一个缩写或特定领域的名词,但目前没有足够的信息来确认其具体含义,因此我们...对于前端开发者来说,了解和掌握异步编程是必不可少的技能,这将有助于提升前端应用的性能和用户体验。
【jsces 包【JeffreyZhao-jscex-v0.5.0-stable-37-g99f0994】】是一款基于JavaScript的轻量级编译器和执行环境,由开发者Jeffrey Zhao精心打造。这个版本号"v0.5.0-stable-37-g99f0994"揭示了软件的稳定性和开发迭代...
Wind.js 是一个基于 JavaScript 的异步处理库,由中国人自主研发并开源,旨在为开发者提供更加高效、便捷的异步编程工具。Wind.js 的设计灵感可能来源于对现有 JavaScript 异步模型的反思,如回调函数、Promise 和 ...
- **编译工具**:使用像 Jscex 或 streamline.js 这样的工具来帮助管理和优化异步代码。 - **辅助函数库**:Async.js 和 Flow-JS 提供了一系列工具函数来简化异步编程。 - **EventProxy**:一种轻量级的事件代理...
在网页设计与开发中,吸引用户的交互元素往往能提升用户体验,其中“环形旋转”效果就是一种常见的动态展示方式。本篇文章将深入探讨如何利用jQuery库来实现页面右下角的环形旋转出分享列表的功能。 一、jQuery简介...
HTML5实验室——Canvas世界(源码)easel.js jscex.jscexRequire.min.js JSLINQ.js processing.js Vector2.js Vector3.js
例如,他们可能用HTML5、CSS3和JavaScript构建一个动态的网页,通过动画效果和音乐来传递情感;或者利用Python、Java等编程语言写一个小程序,让接收者通过解谜来感受爱意。 表白的过程中,数据结构和算法也可能...
Jscex可能是某种JavaScript库或框架,用于简化canvas编程或提供额外功能。 综上所述,实现这个“有底坐大风车动画”不仅需要理解HTML5 canvas的基本绘图原理,还需要掌握动画的制作技巧以及可能涉及的库的使用。...
唯美动画效果,非常试用于表白。妈妈再也不用担心没有女朋友了
Jscex可能是一个JavaScript的表达式引擎,用于处理复杂逻辑或计算。 为了创建大风车动画,我们需要考虑以下步骤: 1. 创建canvas元素并获取2D渲染上下文。 2. 设定canvas的尺寸,使其适应窗口大小或特定容器。 3. ...