js中用来发送activemq异步发送要求的是哪一个对象

8114人阅读
我们经常说JS是单线程的,比如node.js研讨会上大家都说JS的特色之一是单线程的,这样使JS更简单明了,可是大家真的理解所谓JS的单线程机制吗?单线程时,基于事件的异步机制又该当如何,这些知识在《JavaScript权威指南》并没有介绍,我也一直困惑了,直到看到一篇外文,才有了些眉目,这里与大家分享下。翻译的过程中,发现已有人翻译了这篇文章,于是乎,在某些语句上,借鉴了下。文章网址:。后来发现《JavaScript高级程序设计》高级定时器和循环定时器介绍过,不过觉得没我翻译这篇原文介绍得更透彻,觉得我写的不好的,可以查看。
1先看下两个例子
1.1.简单的settimeout&
setTimeout(function () { while (true) { } }, 1000);
setTimeout(function () { alert('end 2'); }, 2000);
setTimeout(function () { alert('end 1'); }, 100);
alert('end');
  执行的结果是弹出’end’、’end
1’,然后浏览器假死,就是不弹出‘end 2’。也就是说第一个settimeout里执行的时候是一个死循环,这个直接导致了理论上比它晚一秒执行的第二个settimeout里的函数被阻塞,这个和我们平时所理解的异步函数多线程互不干扰是不符的。
附计时器使用方法
--初始化一个简单的js的计时器,一段时间后,才触发并执行回调函数。&setTimeout&返回一个唯一id,可用这个id来取消这个计时器。
var id = setTimeout(fn,delay);
--类似于setTimeout,不一样的是,每隔一段时间,会持续调用回调fn,直到被取消
var id = setInterval(fn,delay);
--传入一个计时器的id,取消计时器。
clearInterval(id);
clearTimeout(id);
1.2.ajax请求回调
接着我们来测试一下通过xmlhttprequest实现ajax异步请求调用,主要代码如下:
var xmlReq = createXMLHTTP();//创建一个xmlhttprequest对象
function testAsynRequest() {
&&& var url = &/AsyncHandler.ashx?action=ajax&;
&&& xmlReq.open(&post&, url, true);
&&& xmlReq.setRequestHeader(&Content-Type&, &application/x-www-form-urlencoded&);
&&& xmlReq.onreadystatechange = function () {
&&&&&&& if (xmlReq.readyState == 4) {
&&&&&&&&&&& if (xmlReq.status == 200) {
&&&&&&&&&&&&&&& var jsonData = eval('(' + xmlReq.responseText + ')');
&&&&&&&&&&&&&&& alert(jsonData.message);
&&&&&&&&&&& }
&&&&&&&&&&& else if (xmlReq.status == 404) {
&&&&&&&&&&&&&&& alert(&Requested URL is not found.&);
&&&&&&&&&&& } else if (xmlReq.status == 403) {
&&&&&&&&&&&&&&& alert(&Access denied.&);
&&&&&&&&&&& } else {
&&&&&&&&&&&&&&& alert(&status is & + xmlReq.status);
&&&&&&&&&&& }
&&& xmlReq.send(null);
testAsynRequest();//1秒后调用回调函数
while (true) {
在服务端实现简单的输出:
private void ProcessAjaxRequest(HttpContext context)
&&& string action = context.Request[&ajax&];
&&& Thread.Sleep(1000);//等1秒
&&& string jsonObject = &{\&message\&:\&& + action + &\&}&;
&&& context.Response.Write(jsonObject);
&  理论上,如果ajax异步请求,它的异步回调函数是在单独一个线程中,那么回调函数必然不被其他线程”阻挠“而顺利执行,也就是1秒后,它回调执行弹出‘ajax’,可是实际情况并非如此,回调函数无法执行,因为浏览器再次因为死循环假死。
&  据上面两个例子,总结如下:
①JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序.
②JavaScript引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化。
2.JavaScript引擎
  可JS内部究竟如何实现,我们在接下来探讨。
  在了解计时器内部运作前,我们必须清楚一点,触发和执行并不是同一概念,计时器的回调函数一定会在指定delay的时间后被触发,但并不一定立即执行,可能需要等待。所有JavaScript代码是在一个线程里执行的,像鼠标点击和计时器之类的事件只有在JS单线程空闲时才执行。
  我们来看一下图表,一开始你可能并没发现什么或啥都不懂,但请静下心来,在脑海里绘制出这个场景
  这个图表中有许多数据信息等着我们去理解,当你完全理解了这个图,你会对js的异步运行机制(即JavaScript引擎如何实现异步事件)有很好的了解。这个图是一维的,垂直线上是以毫秒计位,蓝色块代表被划分的不同的js区域执行代码。例如,第一个JS区块执行了18毫秒,鼠标点击事件被阻塞了将近11毫秒,等等。
  由于JavaScript引擎同一时间只执行一段代码(这是由JavaScript单线程的性质决定的),所以每个JS代码块阻塞了其它异步事件的进行。这意味着当一个异步事件(像鼠标点击、计时器、Ajax)发生时,这些事件的回调函数将排在队列后面等待执行(如何排队完全取决于各浏览器,而我们可以忽视它们内部差异,作一个简化处理)。&
  我们首先从第一个JS代码块开始,有两个计时器被初始化:一个10ms的setTimeout和一个10ms的setInterval.观察计时器初始化位置,(计时器初始化完毕后就会开始计时),发现setTimeout计时器的回调实际上会在第一个代码块执行完毕前被触发。但是这里注意的是,它不会立即执行(单线程不能这样做)。实际上,触发的回调将被排成一个队列,等待下一个可执行时间。
  此外,在第一个JS代码块,我们发现一个鼠标点击事件被触发。这个鼠标点击JS回调被绑定在异步队列上(我们从来不知道用户什么时候执行这个操作,所以它被认为是异步的)且不能马上执行。像初始化的计时器一样,排队等待执行。
  执行完初始化JS代码块后,浏览器就有个疑问:谁在等待执行?此时,鼠标点击回调和setTimeout计时器的回调都在等待。浏览器将选一个(鼠标点击事件)并立马执行。而计时器的回调将等待下一合适时机执行。
  注意,鼠标点击事件执行过程中,interval的回调第一次被触发,与setTimeout的回调一样,排队等待执行。随着时间推移,等到setTimeout计时器的回调执行时候,setInterval的回调再次被触发,这次被触发的回调将被抛弃。如果一大段代码块正在执行,所有的setInterval的回调都将要排队,一旦大段代码块执行完毕,这些一连串的setInterval的回调相互间将被无延迟地执行。实际上,浏览器处理setInterval被触发的回调排队等待执行时,除非队列中setInterval回调为空,才允许新的setInterval的回调加入。
  我们发现,setInterval的第一个被触发的回调执行时,setInterval的回调又被触发且排到队列。这向我们传达一个重要的消息:setInterval不关心目前JS正在执行的内容,setInterval的被触发的回调都将会无差别地排队。
  最后,当setInterval的回调执行两次后,我们发现没有javascript引擎要执行东西。这意味着浏览器将等待着一个新的异步事件发生。我们知道,在50ms时候,setInterval的回调再次被触发,但这次并没有东西阻塞,所以回调就立马执行了。&
  在浏览器中,JavaScript引擎是基于事件驱动的,这里的事件可看作是浏览器派给它的各种任务,这些任务可能源自当前执行的代码块,如调用setTimeout(),也可能来自浏览器内核,如onload()、onclick()、onmouseover()、setTimeOut()、setInterval()、Ajax等。如果从代码的角度来看,所谓的任务实体就是各种回调函数,由于“单线程”的原因,这些任务会进行排队,一个接着一个等待着被引擎处理。(这段说法来源于http://www.benben.cc/blog/?p=327)
3.引擎线程和其它侦听线程
上图中,定时器和事件都按时触发了,这表明JavaScript引擎的线程和计时器触发线程、事件触发线程是三个单独的线程,即使JavaScript引擎的线程被阻塞,其它两个触发线程都在运行。
  浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。假如某一浏览器内核的实现至少有三个常驻线程:&JavaScript引擎线程,事件触发线程,Http请求线程,下面通过一个图来阐明单线程的JavaScript引擎与另外那些线程是怎样互动通信的。虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异。
&&&&&线程间通信:JavaScript引擎执行当前的代码块,其它诸如setTimeout给JS引擎添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发器时间到达通知,异步请求状态变更通知等.从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来.由于单线程关系,这些任务得进行排队,一个接着一个被引擎处理.
GUI渲染也是在引擎线程中执行的,脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来。来看例子(这块内容还有待验证,个人觉得当Dom渲染时,才可阻止渲染)
&div id=&test&&test&/div&
&script type=&text/javascript& language=&javascript&&
while(1) {
&&& document.getElementById(&test&).innerHTML+=i++ + &&br /&&;
  这段代码的本意是从0开始顺序显示数字,它们将一个接一个出现,现在我们来仔细研究一下代码,while(1)创建了一个无休止的循环,但是对于单线程的JavaScript引擎而言,在实际情况中就会造成浏览器暂停响应并处于假死状态。
  alert()会停止JS引擎的执行,直到按确认键,在JS调试的时候,查看当前实时页面的内容。
4.setTimeout和&setInterval
回到文章开头,我们来看下setTimeout和setsetInterval的区别。
setTimeout(function(){
&&& /* Some long block of code ... */
&&& setTimout(arguments.callee,10);
setInterval(function(){
&&& /* Some long block of code ... */
  这两个程序段第一眼看上去是一样的,但并不是这样。setTimeout代码至少每隔10ms以上才执行一次;然而setInterval固定每隔10ms将尝试执行,不管它的回调函数的执行状态。
我们来总结下:
l&JavaScript引擎只有一个线程,强制异步事件排队等待执行。
l&setTimeout和setInterval在异步执行时,有着根本性不同。
l&如果一个计时器被阻塞执行,它将会延迟,直到下一个可执行点(这可能比期望的时间更长)
l&setInterval的回调可能被不停的执行,中间没间隔(如果回调执行的时间超过预定等待的值)
《JavaScript高级程序设计》中,针对setInterval说法如下:
当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。还要注意两问题:
①某些间隔会被跳过(抛弃);
②&多个定时器的代码执行之间的间隔可能会比预期小。此时可采取&setTimeout和setsetInterval的区别&的例子方法。
5.Ajax异步
  很多同学朋友搞不清楚,既然说JavaScript是单线程运行的,那么XMLHttpRequest在连接后是否真的异步?其实请求确实是异步的,不过这请求是由浏览器新开一个线程请求(参见上图),当请求的状态变更时,如果先前已设置回调,这异步线程就产生状态变更事件放到JavaScript引擎的处理队列中等待处理,当任务被处理时,JavaScript引擎始终是单线程运行回调函数,具体点即还是单线程运行onreadystatechange所设置的函数。
  Tip:理解JavaScript引擎运作非常重要,特别是在大量异步事件(连续)发生时,可以提升程序代码的效率。
原外文:http://ejohn.org/blog/how-javascript-timers-work/
翻译参考:http://www.phpweblog.net/rainman/archive//6267.html
部分示例:/jeffwongishandsome/archive//2080145.html
其它参考://1089.html
       http://www.benben.cc/blog/?p=327
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:12341次
排名:千里之外
原创:10篇
(10)(2)(1)原生js发送ajax请求 - 漫游CHEER - 博客园
Blog Stats
Posts - 10
Stories - 0
Comments - 1
Trackbacks - 0
& & & & 堕落了一阵子了,今天打开博客,发现连登录的用户名和密码都不记得了。2016年已过半,不能再这么晃荡下去了。
& & & & 参加了网易微专业-前端攻城狮 培训,目前进行到大作业开发阶段,感觉举步维艰。但是无论如何,不管结果怎样,一定要完成这次任务&&毕竟是花了银子的,不能浪费。所以准备写一个系列博客,把开发过程中遇到的各种小问题记录下来,也算是从头到尾做一个开发备案吧。通读了开发要求,大致做了一下***,打算拆分成一个个的小模块,逐个击破。今天是准备阶段,想先试着调一下接口,看看能否调的通。
& & & & 言归正传。从页面向服务器发送请求,当然少不了ajax方法。此处我们不再从头到尾扯历史、扯概念,直接上手,一切向生产力看齐。通常发送ajax请求,都会使用jQuery方法,但是大作业里不允许使用任何js框架,所以此处使用原生js发送请求。
& & & & 归纳起来步骤如下:
& & & & 1. 建立XMLHttpRequest对象
& & & & 此处要注意有兼容性问题(对IE7及以下浏览器写法有不同),实现代码如下:
1 &script&
if(window.XMLHttpRequest){
xmlHttp = new XMLHttpRequest();
}else{//对IE7及以下版本浏览器做兼容
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
8 &/script&
& & & & 2. 建立连接,发送请求及参数
& & & &此处用到ajax的一些方法。其实主要是request.open()方法。请求分为"GET"与"POST"两种形式。"GET"用来获取服务器返回的一些参数,而"POST"方法则允许用户修改服务器上的一些数据。
*open方法解释
共三个参数
*第一个参数:使用的方法,GET
*第二个参数:url地址
*第三个参数:同步方式
异步方式,一般置为true,为异步;默认也为异步调用
request.open("GET"," /webDev/dhuai",true);
*send方法解释
*发送参数,一般针对于POST方法。使用GET方法时,此参数传null或不传值
request.send(null);
& & & & 3. 建立响应信息
& & & & 对服务器返回的状态进行判断,若成功,则拿到数据,进行后续的事宜。这里主要是对request.readyState和request.status两个属性进行判断。
& & & &&request.readyState:状态码属性,枚举如下:
& & & & 0:请求未初始化,open还没有调用
& & & & 1:服务器连接已建立,open已调用
& & & & 2:请求已接收,即收到头信息了
& & & & 3:请求处理中,即接收到响应主体了
& & & & 4:请求已完成,且响应已就绪,即响应完成了
& & & &&request.status:状态值,比较多,在文章结尾处贴出全部状态值枚举,此处先上代码。
request.onreadystatechange = function() {
if (request.readyState==4) {//请求完成
if (request.status==200) { //OK
var data = JSON.parse(request.responseText);//将返回的数据放在data变量中
if (data.success) {
document.getElementById("XXX").innerHTML = data.
document.getElementById("XXX").innerHTML = "出现错误:" + data.
alert("发生错误:" + request.status);
16 &/script&
& & & & 至此,一个完整的ajax请求已发送,并对返回信息做了处理。上面用到了JSON.parse方法,意为将字符串解析成json对象,以便在程序中使用。完整的代码如下:
1 &script&
if(window.XMLHttpRequest){
xmlHttp = new XMLHttpRequest();
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
request.open("GET", "/fafhfg",true);
request.send(null);
request.onreadystatechange = function() {
if (request.readyState==4) {
if (request.status==200) {
var data = JSON.parse(request.responseText);
if (data.success) {
document.getElementById("XXX").innerHTML = data.
document.getElementById("XXX").innerHTML = "出现错误:" + data.
alert("发生错误:" + request.status);
24 &/script&
& & & & 亲测可用,调起了大作业中的第一个接口,并获取到了数据。悲催的是,谷歌上没问题,但是在火狐上发生了跨域拦截。火狐对安全性的要求高,所以有这个限制。尝试了几种方法,并没有解决。不过作业文档中说不考虑跨域问题,所以此处不再深究了。
& & & & 附:state状态值
  &100 - Continue 初始的请求已经接受,客户应当继续发送请求的其余部分。(HTTP 1.1新)   &101 - Switching Protocols 服务器将遵从客户的请求转换到另外一种协议(HTTP 1.1新)&
  &200 - OK 一切正常,对GET和POST请求的应答文档跟在后面。   &201 - Created 服务器已经创建了文档,Location头给出了它的URL。   &202 - Accepted 已经接受请求,但处理尚未完成。   &203 - Non-Authoritative Information 文档已经正常地返回,但一些应答头可能不正确,因为使用的是文档的拷贝,非权威性信息(HTTP 1.1新)。  &204 - No Content 没有新文档,浏览器应该继续显示原来的文档。如果用户定期地刷新页面,而Servlet可以确定用户文档足够新,这个状态代码是很有用的。   &205 - Reset Content 没有新的内容,但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容(HTTP 1.1新)。   &206 - Partial Content 客户发送了一个带有Range头的GET请求,服务器完成了它(HTTP 1.1新)
  &300 - Multiple Choices 客户请求的文档可以在多个位置找到,这些位置已经在返回的文档内列出。如果服务器要提出优先选择,则应该在Location应答头指明。   &301 - Moved Permanently 客户请求的文档在其他地方,新的URL在Location头中给出,浏览器应该自动地访问新的URL。   &302 - Found 类似于301,但新的URL应该被视为临时性的替代,而不是永久性的。注意,在HTTP1.0中对应的状态信息是&Moved
Temporatily&。出现该状态代码时,浏览器能够自动访问新的URL,因此它是一个很有用的状态代码。注意这个状态代码有时候可以和301替换使
用。例如,如果浏览器错误地请求 http://host/~user
(缺少了后面的斜杠),有的服务器返回301,有的则返回302。严格地说,我们只能假定只有当原来的请求是GET时浏览器才会自动重定向。请参见
307。   &303 - See Other 类似于301/302,不同之处在于,如果原来的请求是POST,Location头指定的重定向目标文档应该通过GET提取(HTTP 1.1新)。   &304 - Not Modified 客户端有缓冲的文档并发出了一个条件性的请求(一般是提供If-Modified-Since头表示客户只想比指定日期更新的文档)。服务器告诉客户,原来缓冲的文档还可以继续使用。  &305 - Use Proxy 客户请求的文档应该通过Location头所指明的代理服务器提取(HTTP 1.1新)。   &307 - Temporary Redirect 和302(Found)相同。许多浏览器会错误地响应302应答重
定向,即使原来的请求是POST,即使它实际上只能在POST请求的应答是303时才能重定向。由于这个原因,HTTP
1.1新增了307,以便更加清除地区分几个状态代码:当出现303应答时,浏览器可以跟随重定向的GET和POST请求;如果是307应答,则浏览器只
能跟随对GET请求的重定向。(HTTP 1.1新)   &400 - Bad Request 请求出现错误。   &401 - Unauthorized 访问被拒绝,客户试图未经授权访问受密码的
页面。应答中会包含一个WWW-Authenticate头,浏览器据此显示用户名字/密码对话框,然后在填写合适的Authorization头后再次
发出请求。IIS 定义了许多不同的 401 错误,它们指明更为具体的错误原因。这些具体的错误代码在浏览器中显示,但不在 IIS 日志中显示:  &401.1 - 登录失败。  &401.2 - 服务器配置导致登录失败。  &401.3 - 由于 ACL 对资源的限制而未获得授权。  &401.4 - 筛选器授权失败。  &401.5 - ISAPI/CGI 应用程序授权失败。  &401.7 & 访问被 Web 服务器上的 URL 授权策略拒绝。这个错误代码为 IIS 6.0 所专用。  &403 - Forbidden 资源不可用。服务器理解客户的请求,但拒绝处理它。通常由于服务器上文件或目录的权限设置导致。禁止访问:IIS 定义了许多不同的 403 错误,它们指明更为具体的错误原因:  &403.1 - 执行访问被禁止。  &403.2 - 读访问被禁止。  &403.3 - 写访问被禁止。  &403.4 - 要求 SSL。  &403.5 - 要求 SSL 128。  &403.6 - IP 地址被拒绝。  &403.7 - 要求客户端***。  &403.8 - 站点访问被拒绝。  &403.9 - 用户数过多。  &403.10 - 配置无效。  &403.11 - 密码更改。  &403.12 - 拒绝访问映射表。  &403.13 - 客户端***被吊销。  &403.14 - 拒绝目录列表。  &403.15 - 超出客户端访问许可。  &403.16 - 客户端***不受信任或无效。  &403.17 - 客户端***已过期或尚未生效。  &403.18 - 在当前的应用程序池中不能执行所请求的 URL。这个错误代码为 IIS 6.0 所专用。  &403.19 - 不能为这个应用程序池中的客户端执行 CGI。这个错误代码为 IIS 6.0 所专用。  &403.20 - Passport 登录失败。这个错误代码为 IIS 6.0 所专用。  &404 - Not Found 无法找到指定位置的资源。这也是一个常用的应答。   &404.0 -(无) & 没有找到文件或目录。  &404.1 - 无法在所请求的端口上访问 Web 站点。  &404.2 - Web 服务扩展锁定策略阻止本请求。  &404.3 - MIME 映射策略阻止本请求。  &405 - Method Not Allowed 请求方法(GET、POST、HEAD、DELETE、PUT、TRACE等)对指定的资源不适用,用来访问本页面的 HTTP 谓词不被允许(方法不被允许)(HTTP 1.1新)   &406 - Not Acceptable 指定的资源已经找到,但它的MIME类型和客户在Accpet头中所指定的不兼容,客户端浏览器不接受所请求页面的 MIME 类型(HTTP 1.1新)。   &407 - Proxy Authentication Required 要求进行代理身份验证,类似于401,表示客户必须先经过代理服务器的授权。(HTTP 1.1新)   &408 - Request Timeout 在服务器许可的等待时间内,客户一直没有发出任何请求。客户可以在以后重复同一请求。(HTTP 1.1新)  &409 - Conflict 通常和PUT请求有关。由于请求和资源的当前状态相冲突,因此请求不能成功。(HTTP 1.1新)   &410 - Gone 所请求的文档已经不再可用,而且服务器不应该重定向到哪一个地址。它和404的不同在于,返回407表示文档永久地离开了指定的位置,而404表示由于未知的原因文档不可用。(HTTP 1.1新)   &411 - Length Required 服务器不能处理请求,除非客户发送一个Content-Length头。(HTTP 1.1新)   &412 - Precondition Failed 请求头中指定的一些前提条件失败(HTTP 1.1新)。  &413 & Request Entity Too Large 目标文档的大小超过服务器当前愿意处理的大小。如果服务器认为自己能够稍后再处理该请求,则应该提供一个Retry-After头(HTTP 1.1新)。   &414 - Request URI Too Long URI太长(HTTP 1.1新)。   &415 & 不支持的类型。  &416 & Requested Range Not Satisfiable 服务器不能满足客户在请求中指定的Range头。(HTTP 1.1新)   &417 & 执行失败。  &423 & 锁定的错误。  &500 - Internal Server Error 服务器遇到了意料不到的情况,不能完成客户的请求。   &500.12 - 应用程序正忙于在 Web 服务器上重新启动。  &500.13 - Web 服务器太忙。  &500.15 - 不允许直接请求 Global.asa。  &500.16 & UNC 授权凭据不正确。这个错误代码为 IIS 6.0 所专用。  &500.18 & URL 授权存储不能打开。这个错误代码为 IIS 6.0 所专用。  &500.100 - 内部 ASP 错误。  &501 - Not Implemented 服务器不支持实现请求所需要的功能,页眉值指定了未实现的配置。例如,客户发出了一个服务器不支持的PUT请求。  &502 - Bad Gateway 服务器作为网关或者代理时,为了完成请求访问下一个服务器,但该服务器返回了非法的应答。 亦说Web 服务器用作网关或代理服务器时收到了无效响应。  &502.1 - CGI 应用程序超时。  &502.2 - CGI 应用程序出错。  &503 - Service Unavailable 服务不可用,服务器由于维护或者负载过重未能应答。例如,Servlet可能在库连接池已满的情况下返回503。服务器返回503时可以提供一个Retry-After头。这个错误代码为 IIS 6.0 所专用。  &504 - Gateway Timeout 网关超时,由作为代理或网关的服务器使用,表示不能及时地从远程服务器获得应答。(HTTP 1.1新) 。& & &505 - HTTP Version Not Supported 服务器不支持请求中所指明的HTTP版本。&你不懂JS: 异步与性能 第一章: 异步: 现在与稍后 - 简书
下载简书移动应用
写了279357字,被455人关注,获得了242个喜欢
你不懂JS: 异步与性能 第一章: 异步: 现在与稍后
在像JavaScript这样的语言中最重要但经常被误解的编程技术之一,就是如何表达和操作跨越一段时间的程序行为。
这不仅仅是关于从for循环开始到for循环结束之间发生的事情,当然它确实要花 一些时间(几微秒到几毫秒)才能完成。 它是关于你的程序 现在 运行的部分,和你的程序 稍后 运行的另一部分之间发生的事情----现在 和 稍后 之间有一个间隙,在这个间隙中你的程序没有活跃地执行。
几乎所有被编写过的(特别是用JS)大型程序都不得不用这样或那样的方法来管理这个间隙,不管是等待用户输入,从数据库或文件系统请求数据,通过网络发送数据并等待应答,还是在规定的时间间隔重复某些任务(比如动画)。在所有这些各种方法中,你的程序都不得不跨越时间间隙管理状态。就像在伦敦众所周知的一句话(地铁门与月台间的缝隙):“小心间隙。”
实际上,你程序中 现在 与 稍后 的部分之间的关系,就是异步编程的核心。
可以确定的是,异步编程在JS的最开始就出现了。但是大多数开发者从没认真地考虑过它到底是如何,为什么出现在他们的程序中的,也没有探索过 其他 处理异步的方式。足够好 的方法总是老实巴交的回调函数。今天还有许多人坚持认为回调就绰绰有余了。
但是JS在使用范围和复杂性上不停地生长,作为运行在浏览器,服务器和每种可能的设备上的头等编程语言,为了适应它不断扩大的要求,我们在管理异步上感受到的痛苦日趋严重,人们迫切地需要一种更强大更合理的处理方法。
虽然眼前这一切看起来很抽象,但我保证,随着我们通读这本书你会更完整且坚实地解决它。在接下来的几章中我们将会探索各种异步JavaScript编程的新兴技术。
但在接触它们之前,我们将不得不更深刻地理解异步是什么,以及它在JS中如何运行。
块儿(Chunks)中的程序
你可能将你的JS程序写在一个 .js 文件中,但几乎可以确定你的程序是由几个代码块儿构成的,仅有其中的一个将会在 现在 执行,而其他的将会在 稍后 执行。最常见的 代码块儿 单位是function。
大多数刚接触JS的开发者都可能会有的问题是,稍后 并不严格且立即地在 现在 之后发生。换句话说,根据定义,现在 不能完成的任务将会异步地完成,而且我们因此不会有你可能在直觉上期望或想要的阻塞行为。
考虑这段代码:
// ajax(..)是某个包中任意的Ajax函数
var data = ajax( "http://some.url.1" );
console.log( data );
// 噢!`data`一般不会有Ajax的结果
你可能意识到Ajax请求不会同步地完成,这意味着ajax(..)函数还没有任何返回的值可以赋值给变量data。如果ajax(..)在应答返回之前 能够 阻塞,那么data = ..赋值将会正常工作。
但那不是我们使用Ajax的方式。我们 现在 制造一个异步的Ajax请求,直到 稍后 我们才会得到结果。
从 现在 “等到” 稍后 最简单的(但绝对不是唯一的,或最好的)方法,通常称为回调函数:
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", function myCallbackFunction(data){
console.log( data ); // Yay, 我得到了一些`data`!
警告: 你可能听说过发起同步的Ajax请求是可能的。虽然在技术上是这样的,但你永远,永远不应该在任何情况下这样做,因为它将锁定浏览器的UI(按钮,菜单,滚动条,等等)而且阻止用户与任何东西互动。这是一个非常差劲的主意,你应当永远回避它。
在你提出抗议之前,不,你渴望避免混乱的回调不是使用阻塞的,同步的Ajax的正当理由。
举个例子,考虑下面的代码:
function now() {
return 21;
function later() {
answer = answer * 2;
console.log( "Meaning of life:", answer );
var answer = now();
setTimeout( later, 1000 ); // Meaning of life: 42
这个程序中有两个代码块儿:现在 将会运行的东西,和 稍后 将会运行的东西。这两个代码块分别是什么应当十分明显,但还是让我们以最明确的方式指出来:
function now() {
return 21;
function later() { .. }
var answer = now();
setTimeout( later, 1000 );
answer = answer * 2;
console.log( "Meaning of life:", answer );
你的程序一执行,现在 代码块儿就会立即运行。但setTimeout(..)还设置了一个 稍后 会发生的事件(一个超时事件),所以later()函数的内容将会在一段时间后(从现在开始1000毫秒)被执行。
每当你将一部分代码包进function并且规定它应当为了响应某些事件而执行(定时器,鼠标点击,Ajax应答等等),你就创建了一个 稍后 代码块儿,也因此在你的程序中引入了异步。
异步控制台
关于console.*方法如何工作,没有相应的语言规范或一组需求----它们不是JavaScript官方的一部分,而是由 宿主环境 添加到JS上的(见本丛书的 类型与文法)。
所以,不同的浏览器和JS环境各自为战,这有时会导致令人困惑的行为。
特别地,有些浏览器和某些条件下,console.log(..)实际上不会立即输出它得到的东西。这个现象的主要原因可能是因为I/O处理很慢,而且是许多程序的阻塞部分(不仅是JS)。所以,对一个浏览器来说,可能的性能更好的处理方式是(从网页/UI的角度看),在后台异步地处理consoleI/O,而你也许根本不知道它发生了。
虽然不是很常见,但是一种可能被观察到(不是从代码本身,而是从外部)的场景是:
console.log( a ); // ??
a.index++;
我们一般希望看到的是,就在console.log(..)语句被执行的那一刻,对象a被取得一个快照,打印出如{ index: 1 }的内容,如此在下一个语句a.index++执行时,它修改不同于a的输出,或者严格的在a的输出之后的某些东西。
大多数时候,上面的代码将会在你的开发者工具控制台中产生一个你期望的对象表现形式。但是同样的代码也可能运行在这样的情况下:浏览器告诉后台它需要推迟控制台I/O,这时,在对象在控制台中被表示的那个时间点,a.index++已经执行了,所以它将显示{ index: 2 }。
到底在什么条件下consoleI/O将被推迟是不确定的,甚至它能不能被观察到都是不确定的。只能当你在调试过程中遇到问题时----对象在console.log(..)语句之后被修改,但你却意外地看到了修改后的内容----意识到I/O的这种可能的异步性。
注意: 如果你遇到了这种罕见的情况,最好的选择是使用JS调试器的断点,而不是依赖console的输出。第二好的选择是通过将目标对象序列化为一个string强***得一个它的快照,比如用JSON.stringify(..)。
事件轮询(Event Loop)
让我们来做一个(也许是令人震惊的)声明:尽管明确地允许异步JS代码(就像我们刚看到的超时),但是实际上,直到最近(ES6)为止,JavaScript本身从来没有任何内建的异步概念。
什么!? 这听起来简直是疯了,对吧?事实上,它是真的。JS引擎本身除了在某个在被要求的时刻执行你程序的一个单独的代码块外,没有做过任何其他的事情。
“被'谁'要求”?这才是重要的部分!
JS引擎没有运行在隔离的区域。它运行在一个 宿主环境 中,对大多数开发者来说这个宿主环境就是浏览器。在过去的几年中(但不特指这几年),JS超越了浏览器的界限进入到了其他环境中,比如服务器,通过Node.js这样的东西。其实,今天JavaScript已经被嵌入到所有种类的设备中,从机器人到电灯泡儿。
所有这些环境的一个共通的“线程”(一个“不那么微妙”的异步玩笑,不管怎样)是,他们都有一种机制:在每次调用JS引擎时,可以 随着时间的推移 执行你的程序的多个代码块儿,这称为“事件轮询(Event Loop)”。
换句话说,JS引擎对 时间 没有天生的感觉,反而是一个任意JS代码段的按需执行环境。是它周围的环境在不停地安排“事件”(JS代码的执行)。
那么,举例来说,当你的JS程序发起一个从服务器取得数据的Ajax请求时,你在一个函数(通常称为回调)中建立好“应答”代码,然后JS引擎就会告诉宿主环境,“嘿,我就要暂时停止执行了,但不管你什么时候完成了这个网络请求,而且你还得到一些数据的话,请 回来调 这个函数。”
然后浏览器就会为网络的应答设置一个***器,当它有东西要交给你的时候,它会通过将回调函数插入 事件轮询 来安排它的执行。
那么什么是 事件轮询?
让我们先通过一些假想代码来对它形成一个概念:
// `eventLoop`是一个像队列一样的数组(先进先出)
var eventLoop = [ ];
// “永远”执行
while (true) {
// 执行一个"tick"
if (eventLoop.length & 0) {
// 在队列中取得下一个事件
event = eventLoop.shift();
// 现在执行下一个事件
catch (err) {
reportError(err);
当然,这只是一个用来展示概念的大幅简化的假想代码。但是对于帮助我们建立更好的理解来说应该够了。
如你所见,有一个通过while循环来表现的持续不断的循环,这个循环的每一次迭代称为一个“tick”。在每一个“tick”中,如果队列中有一个事件在等待,它就会被取出执行。这些事件就是你的函数回调。
很重要并需要注意的是,setTimeout(..)不会将你的回调放在事件轮询队列上。它设置一个定时器;当这个定时器超时的时候,环境才会把你的回调放进事件轮询,这样在某个未来的tick中它将会被取出执行。
如果在那时事件轮询队列中已经有了20个事件会怎么样?你的回调要等待。它会排到队列最后----没有一般的方法可以插队和跳到队列的最前方。这就解释了为什么setTimeout(..)计时器可能不会完美地按照预计时间触发。你得到一个保证(粗略地说):你的回调不会再你指定的时间间隔之前被触发,但是可能会在这个时间间隔之后被触发,具体要看事件队列的状态。
换句话说,你的程序通常被打断成许多小的代码块儿,它们一个接一个地在事件轮询队列中执行。而且从技术上说,其他与你的程序没有直接关系的事件也可以穿插在队列中。
注意: 我们提到了“直到最近”,暗示着ES6改变了事件轮询队列在何处被管理的性质。这主要是一个正式的技术规范,ES6现在明确地指出了事件轮询应当如何工作,这意味着它技术上属于JS引擎应当关心的范畴内,而不仅仅是 宿主环境。这么做的一个主要原因是为了引入ES6的Promises(我们将在第三章讨论),因为人们需要有能力对事件轮询队列的排队操作进行直接,细粒度的控制(参见“协作”一节中关于setTimeout(..0)的讨论)。
将“异步”与“并行”两个词经常被混为一谈,但它们实际上是十分不同的。记住,异步是关于 现在 与 稍后 之间的间隙。但并行是关于可以同时发生的事情。
关于并行计算最常见的工具就是进程与线程。进程和线程独立地,可能同时地执行:在不同的处理器上,甚至在不同的计算机上,而多个线程可以共享一个进程的内存资源。
相比之下,一个事件轮询将它的工作打碎成一系列任务并串行地执行它们,不允许并行访问和更改共享的内存。并行与“串行”可能以在不同线程上的事件轮询协作的形式共存。
并行线程执行的穿插,与异步事件的穿插发生在完全不同的粒度等级上:
function later() {
answer = answer * 2;
console.log( "Meaning of life:", answer );
虽然later()的整个内容将被当做一个事件轮询队列的实体,但当考虑到将要执行这段代码的线程时,实际上也许会有许多不同的底层操作。比如,answer = answer * 2首先需要读取当前answer的值,再把2放在某个地方,然后进行乘法计算,最后把结果存回到answer。
在一个单线程环境中,线程队列中的内容都是底层操作真的无关紧要,因为没有什么可以打断线程。但如果你有一个并行系统,在同一个程序中有两个不同的线程,你很可能会得到无法预测的行为:
考虑这段代码:
var a = 20;
function foo() {
a = a + 1;
function bar() {
a = a * 2;
// ajax(..) 是一个给定的库中的随意Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
在JavaScript的单线程行为下,如果foo()在bar()之前执行,结果a是42,但如果bar()在foo()之前执行,结果a将是41。
如果JS事件共享相同的并列执行数据,问题将会变得微妙得多。考虑这两个假想代码段,它们分别描述了运行foo()和bar()中代码的线程将要执行的任务,并考虑如果它们在完全相同的时刻运行会发生什么:
线程1(X和Y是临时的内存位置):
a. 将`a`的值读取到`X`
b. 将`1`存入`Y`
c. 把`X`和`Y`相加,将结果存入`X`
d. 将`X`的值存入`a`
线程2(X和Y是临时的内存位置):
a. 将`a`的值读取到`X`
b. 将`2`存入`Y`
c. 把`X`和`Y`相乘,将结果存入`X`
d. 将`X`的值存入`a`
现在,让我们假定这两个线程在并行执行。你可能发现了问题,对吧?它们在临时的步骤中使用共享的内存位置X和Y。
如果步骤像这样发生,a的最终结果什么?
(将`a`的值读取到`X`
(将`a`的值读取到`X`
(将`1`存入`Y`
(将`2`存入`Y`
(把`X`和`Y`相加,将结果存入`X`
(将`X`的值存入`a`
(把`X`和`Y`相乘,将结果存入`X`
(将`X`的值存入`a`
a中的结果将是44。那么这种顺序呢?
(将`a`的值读取到`X`
(将`a`的值读取到`X`
(将`2`存入`Y`
(将`1`存入`Y`
(把`X`和`Y`相乘,将结果存入`X`
(把`X`和`Y`相加,将结果存入`X`
(将`X`的值存入`a`
(将`X`的值存入`a`
a中的结果将是21。
所以,关于线程的编程十分刁钻,因为如果你不采取特殊的步骤来防止这样的干扰/穿插,你会得到令人非常诧异的,不确定的行为。这通常让人头疼。
JavaScript从不跨线程共享数据,这意味着不必关心这一层的不确定性。但这并不意味着JS总是确定性的。记得前面foo()和bar()的相对顺序产生两个不同的结果吗(41或42)?
注意: 可能还不明显,但不是所有的不确定性都是坏的。有时候它无关紧要,有时候它是故意的。我们会在本章和后续几章中看到更多的例子。
运行至完成
因为JavaScript是单线程的,foo()(和bar())中的代码是原子性的,这意味着一旦foo()开始运行,它的全部代码都会在bar()中的任何代码可以运行之前执行完成,反之亦然。这称为“运行至完成”行为。
事实上,运行至完成的语义会在foo()与bar()中有更多的代码时更明显,比如:
var a = 1;
var b = 2;
function foo() {
a = b + 3;
function bar() {
b = a * 2;
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
因为foo()不能被bar()打断,而且bar()不能被foo()打断,所以这个程序根据哪一个先执行只有两种可能的结果----如果线程存在,foo()和bar()中的每一个语句都可能被穿插,可能的结果数量将会极大地增长!
代码块儿1是同步的(现在 发生),但代码块儿2和3是异步的(稍后 发生),这意味着它们的执行将会被时间的间隙分开。
代码块儿1:
var a = 1;
var b = 2;
代码块儿2 (foo()):
a = b + 3;
代码块儿3 (bar()):
b = a * 2;
代码块儿2和3哪一个都有可能先执行,所以这个程序有两个可能的结果,正如这里展示的:
var a = 1;
var b = 2;
a = b + 3;
b = a * 2;
var a = 1;
var b = 2;
b = a * 2;
a = b + 3;
同一段代码有两种结果仍然意味着不确定性!但是这是在函数(事件)顺序的水平上,而不是在使用线程时语句顺序的水平上(或者说,实际上是表达式操作的顺序上)。换句话说,他比线程更具有 确定性。
当套用到JavaScript行为时,这种函数顺序的不确定性通常称为“竞合状态”,因为foo()和bar()在互相竞争看谁会先运行。明确地说,它是一个“竞合状态”因为你不能可靠地预测a与b将如何产生。
注意: 如果在JS中不知怎的有一个函数没有运行至完成的行为,我们会有更多可能的结果,对吧?ES6中引入一个这样的东西(见第四章“生成器”),但现在不要担心,我们会回头讨论它。
让我们想象一个网站,它显示一个随着用户向下滚动而逐步加载的状态更新列表(就像社交网络的新消息)。要使这样的特性正确工作,(至少)需要两个分离的“进程” 同时 执行(在同一个时间跨度内,但没必要是同一个时间点)。
注意: 我们在这里使用带引号的“进程”,因为它们不是计算机科学意义上的真正的操作系统级别的进程。它们是虚拟进程,或者说任务,表示一组逻辑上关联,串行顺序的操作。我们将简单地使用“进程”而非“任务”,因为在术语层面它与我们讨论的概念的定义相匹配。
第一个“进程”将响应当用户向下滚动页面时触发的onscroll事件(发起取得新内容的Ajax请求)。第二个“进程”将接收返回的Ajax应答(将内容绘制在页面上)。
显然,如果用户向下滚动的足够快,你也许会看到在第一个应答返回并处理期间,有两个或更多的onscroll事件被触发,因此你将使onscroll事件和Ajax应答事件迅速触发,互相穿插在一起。
并发是当两个或多个“进程”在同一时间段内同时执行,无论构成它们的各个操作是否 并行地(在同一时刻不同的处理器或内核)发生。你可以认为并发是“进程”级别的(或任务级别)的并行机制,而不是操作级别的并行机制(分割进程的线程)。
注意: 并发还引入了这些“进程”间彼此互动的概念。我们稍后会讨论它。
在一个给定的时间跨度内(用户可以滚动的那几秒),让我们将每个独立的“进程”作为一系列事件/操作描绘出来:
“线程”1 (onscroll事件):
onscroll, request 1
onscroll, request 2
onscroll, request 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
onscroll, request 7
“线程”2 (Ajax应答事件):
response 1
response 2
response 3
response 4
response 5
response 6
response 7
一个onscroll事件与一个Ajax应答事件很有可能在同一个 时刻 都准备好被处理了。比如我们在一个时间线上描绘一下这些事件的话:
onscroll, request 1
onscroll, request 2
response 1
onscroll, request 3
response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
response 4
onscroll, request 7
response 6
response 5
response 7
但是,回到本章前面的事件轮询概念,JS一次只能处理一个事件,所以不是onscroll, request 2首先发生就是response 1首先发生,但是他们不可能完全在同一时刻发生。就像学校食堂的孩子们一样,不管他们在门口挤成什么样,他们最后都不得不排成一个队来打饭!
让我们来描绘一下所有这些事件在事件轮询队列上穿插的情况:
事件轮询队列:
onscroll, request 1
&--- 进程1开始
onscroll, request 2
response 1
&--- 进程2开始
onscroll, request 3
response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
response 4
onscroll, request 7
&--- 进程1结束
response 6
response 5
response 7
&--- 进程2结束
“进程1”和“进程2”并发地运行(任务级别的并行),但是它们的个别事件在事件轮询队列上顺序地运行。
顺便说一句,注意到response 6和response 5没有按照预想的顺序应答吗?
单线程事件轮询是并发的一种表达(当然还有其他的表达,我们稍后讨论)。
在同一个程序中两个或更多的“进程”在穿插它们的步骤/事件时,如果它们的任务之间没有联系,那么他们就没必要互动。如果它们不互动,不确定性就是完全可以接受的。
举个例子:
var res = {};
function foo(results) {
function bar(results) {
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
foo()和bar()是两个并发的“进程”,而且它们被触发的顺序是不确定的。但对我们的程序的结构来讲它们的触发顺序无关紧要,因为它们的行为相互独立所以不需要互动。
这不是一个“竞合状态”Bug,因为这段代码总能够正确工作,与顺序无关。
更常见的是,通过作用域和/或DOM,并发的“进程”将有必要间接地互动。当这样的互动将要发生时,你需要协调这些互动行为来防止前面讲述的“竞合状态”。
这里是两个由于隐含的顺序而互动的并发“进程”的例子,它 有时会出错:
var res = [];
function response(data) {
res.push( data );
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
并发的“进程”是那两个将要处理Ajax应答的response()调用。它们谁都有可能先发生。
假定我们期望的行为是res[0]拥有"http://some.url.1"调用的结果,而res[1]拥有"http://some.url.2"调用的结果。有时候结果确实是这样,而有时候则相反,要看哪一个调用首先完成。很有可能,这种不确定性是一个“竞合状态”Bug。
注意: 在这些情况下要极其警惕你可能做出的主观臆测。比如这样的情况就没什么不寻常:一个开发者观察到"http://some.url.2"的应答“总是”比"http://some.url.1"要慢得多,也许有赖于它们所做的任务(比如,一个执行数据库任务而另一个只是取得静态文件),所以观察到的顺序看起来总是所期望的。就算两个请求都发到同一个服务器,而且它故意以确定的顺序应答,也不能 真正 保证应答回到浏览器的顺序。
所以,为了解决这样的竞合状态,你可以协调互动的顺序:
var res = [];
function response(data) {
if (data.url == "http://some.url.1") {
else if (data.url == "http://some.url.2") {
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
无论哪个Ajax应答首先返回,我们都考察它的data.url(当然,假设这样的数据会从服务器返回)来找到应答数据应当在res数组中占有的位置。res[0]将总是持有"http://some.url.1"的结果,而res[1]将总是持有"http://some.url.2"的结果。通过简单的协调,我们消除了“竞合状态”的不确定性。
这个场景的同样道理可以适用于这样的情况:多个并发的函数调用通过共享的DOM互动,比如一个在更新&div&的内容而另一个在更新&div&的样式或属性(比如一旦DOM元素拥有内容就使它变得可见)。你可能不想在DOM元素拥有内容之前显示它,所以协调工作就必须保证正确顺序的互动。
没有协调的互动,有些并发的场景 总是出错(不仅仅是 有时)。考虑下面的代码:
function foo(x) {
a = x * 2;
function bar(y) {
b = y * 2;
function baz() {
console.log(a + b);
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
在这个例子中,不管foo()和bar()谁先触发,总是会使baz()运行的太早了(a和b之一还是空的时候),但是第二个baz()调用将可以工作,因为a和b将都是可用的。
有许多不同的方法可以解决这个状态。这是简单的一种:
function foo(x) {
a = x * 2;
if (a && b) {
function bar(y) {
b = y * 2;
if (a && b) {
function baz() {
console.log( a + b );
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
baz()调用周围的if (a && b)条件通常称为“大门”,因为我们不能确定a和b到来的顺序,但在打开大门(调用baz())之前我们等待它们全部到达。
另一种你可能会遇到的并发互动状态有时称为“竞争”,单更准确地说应该叫“门闩”。它的行为特点是“先到者胜”。在这里不确定性是可以接受的,因为你明确指出“竞争”的终点线上只有一个胜利者。
考虑这段有问题的代码:
function foo(x) {
a = x * 2;
function bar(x) {
a = x / 2;
function baz() {
console.log( a );
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
不管哪一个函数最后触发(foo()或bar()),它不仅会覆盖前一个函数对a的赋值,还会重复调用baz()(不太可能是期望的)。
所以,我们可以用一个简单的门闩来协调互动,仅让第一个过去:
function foo(x) {
if (a == undefined) {
a = x * 2;
function bar(x) {
if (a == undefined) {
a = x / 2;
function baz() {
console.log( a );
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
if (a == undefined)条件仅会让foo()或bar()中的第一个通过,而第二个(以及后续所有的)调用将会被忽略。第二名什么也得不到!
注意: 在所有这些场景中,为了简化说明的目的我们都用了全局变量,这里我们没有任何理由需要这么做。只要我们讨论中的函数可以访问变量(通过作用域),它们就可以正常工作。依赖于词法作用域变量(参见本丛书的 作用域与闭包 ),和这些例子中实质上的全局变量,是这种并发协调形式的一个明显的缺点。在以后的几章中,我们会看到其他的在这方面干净得多的协调方法。
另一种并发协调的表达称为“协作并发”,它并不那么看重在作用域中通过共享值互动(虽然这依然是允许的!)。它的目标是将一个长时间运行的“进程”打断为许多步骤或批处理,以至于其他的并发“进程”有机会将它们的操作穿插进事件轮询队列。
举个例子,考虑一个Ajax应答处理器,它需要遍历一个很长的结果列表来将值变形。我们将使用Array#map(..)来让代码短一些:
var res = [];
// `response(..)`从Ajax调用收到一个结果数组
function response(data) {
// 连接到既存的`res`数组上
res = res.concat(
// 制造一个新的变形过的数组,所有的`data`值都翻倍
data.map( function(val){
return val * 2;
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
如果"http://some.url.1"首先返回它的结果,整个结果列表将会一次性映射进res。如果只有几千或更少的结果记录,一般来说不是什么大事。但假如有1千万个记录,那么就可能会花一段时间运行(在强大的笔记本电脑上花几秒钟,在移动设备上花的时间长得多,等等)。
当这样的“处理”运行时,页面上没有任何事情可以发生,包括不能有另一个response(..)调用,不能有UI更新,甚至不能有用户事件比如滚动,打字,按钮点击等。非常痛苦。
所以,为了制造协作性更强、更友好而且不独占事件轮询队列的并发系统,你可以在一个异步批处理中处理这些结果,在批处理的每一步都“让出”事件轮询来让其他等待的事件发生。
这是一个非常简单的方法:
var res = [];
// `response(..)`从Ajax调用收到一个结果数组
function response(data) {
// 我们一次只处理1000件
var chunk = data.splice( 0, 1000 );
// 连接到既存的`res`数组上
res = res.concat(
// 制造一个新的变形过的数组,所有的`data`值都翻倍
chunk.map( function(val){
return val * 2;
// 还有东西要处理吗?
if (data.length & 0) {
// 异步规划下一个批处理
setTimeout( function(){
response( data );
// ajax(..) 是某个包中任意的Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
我们以每次最大1000件作为一个块儿处理数据。这样,我们保证每个“进程”都是短时间运行的,即便这意味着会有许多后续的“进程”,在事件轮询队列上的穿插将会给我们一个响应性(性能)强得多的网站/应用程序。
当然,我们没有对任何这些“进程”的顺序进行互动协调,所以在res中的结果的顺序是不可预知的。如果要求顺序,你需要使用我们之前讨论的互动技术,或者在本书后续章节中介绍的其他技术。
我们使用setTimeout(..0)(黑科技)来异步排程,基本上它的意思是“将这个函数贴在事件轮询队列的末尾”。
注意: 从技术上讲,setTimeout(..0)没有直接将一条记录插入事件轮询队列。计时器将会在下一个运行机会将事件插入。比如,两个连续的setTimeout(..0)调用不会严格保证以调用的顺序被处理,所以我们可能看到各种时间偏移的情况,使这样的事件的顺序是不可预知的。在Node.js中,一个相似的方式是process.nextTick(..)。不管那将会有多方便(而且通常性能更好),(还)没有一个直接的方法可以横跨所有环境来保证异步事件顺序。我们会在下一节详细讨论这个话题。
在ES6中,在事件轮询队列之上引入了一层新概念,称为“工作队列(Job queue)”。你最有可能接触它的地方是在Promises(见第三章)的异步行为中。
不幸的是,它目前是一个没有公开API的机制,因此要演示它有些兜圈子。我们不得不仅仅在概念上描述它,这样当我们在第三章中讨论异步行为时,你将会理解那些动作行为是如何排程与处理的。
那么,我能找到的考虑它的最佳方式是:“工作队列”是一个挂靠在事件轮询队列的每个tick末尾的队列。在事件轮询的一个tick期间内,某些可能发生的隐含异步动作的行为将不会导致一个全新的事件加入事件轮询队列,而是在当前tick的工作队列的末尾加入一个新的记录(也就是一个Job)。
它好像是在说,“哦,另一件需要我 稍后 去做的事儿,但是保证它在其他任何事情发生之间发生。”
或者,用一个比喻:事件轮询队列就像一个游乐园项目,一旦你乘坐完一次,你就不得不去队尾排队来乘坐下一次。而工作队列就像乘坐完后,立即插队乘坐下一次。
一个Job还可能会导致更多的Job被加入同一个队列的末尾。所以,一个在理论上可能的情况是,Job“轮询”(一个Job持续不断地加入其他Job等)会无限地转下去,从而拖住程序不能移动到一下一个事件轮询tick。这与在你的代码中表达一个长时间运行或无限循环(比如while (true) ..)在概念上几乎是一样的。
Job的精神有点儿像setTimeout(..0)黑科技,但以一种定义明确得多的方式实现,而且保证顺序: 稍后,但尽快。
让我们想象一个用于Job排程的API,并叫它schedule(..)。考虑如下代码:
console.log( "A" );
setTimeout( function(){
console.log( "B" );
// 理论上的 "Job API"
schedule( function(){
console.log( "C" );
schedule( function(){
console.log( "D" );
你肯能会期望它打印出A B C D,但是它将会打出A C D B,因为Job发生在当前的事件轮询tick的末尾,而定时器会在 下一个 事件轮询tick(如果可用的话!)触发排程。
在第三章中,我们会看到Promises的异步行为是基于Job的,所以搞明白它与事件轮询行为的联系是很重要的。
我们在代码中表达语句的顺序没有必要与JS引擎执行它们的顺序相同。这可能看起来像是个奇怪的论断,所以我们简单地探索一下。
但在我们开始之前,我们应当对一些事情十分清楚:从程序的角度看,语言的规则/文法(参见本丛书的 类型与文法)为语句的顺序决定了一个非常可预知、可靠的行为。所以我们将要讨论的是在你的JS程序中 应当永远观察不到的东西。
警告: 如果你曾经 观察到 过我们将要描述的编译器语句重排,那明显是违反了语言规范,而且无疑是那个JS引擎的Bug----它应当被报告并且修复!但是更常见的是你 怀疑 JS引擎里发生了什么疯狂的事,而事实上它只是你自己代码中的一个Bug(可能是一个“竞合状态”)----所以先检查那里,多检查几遍。在JS调试器使用断点并一行一行地步过你的代码,将是帮你在 你的代码 中找出这样的Bug的最强大的工具。
考虑下面的代码:
a = a + 1;
b = b + 1;
console.log( a + b ); // 42
这段代码没有任何异步表达(除了早先讨论的罕见的console异步I/O),所以最有可能的推测是它会一行一行地、从上到下地处理。
但是,JS引擎 有可能,在编译完这段代码后(是的,JS是被编译的----见本丛书的 作用域与闭包)发现有机会通过(安全地)重新安排这些语句的顺序来使你的代码运行得更快。实质上,只要你观察不到重排,一切都是合理的。
举个例子,引擎可能会发现如果实际上这样执行代码会更快:
console.log( a + b ); // 42
或者是这样:
console.log( a + b ); // 42
或者甚至是:
// 因为`a`和`b`都不再被使用,我们可以内联而且根本不需要它们!
console.log( 42 ); // 42
在所有这些情况下,JS引擎在它的编译期间进行着安全的优化,而最终的 可观察到 的结果将是相同的。
但也有一个场景,这些特殊的优化是不安全的,因而也是不被允许的(当然,不是说它一点儿都没优化):
// 我们需要`a`和`b`递增之前的状态!
console.log( a * b ); // 300
a = a + 1;
b = b + 1;
console.log( a + b ); // 42
编译器重排会造成可观测的副作用(因此绝不会被允许)的其他例子,包括任何带有副作用的函数调用(特别是getter函数),或者ES6的Proxy对象(参见本丛书的 ES6与未来)。
考虑如下代码:
function foo() {
console.log( b );
// ES5.1 getter 字面语法
get bar() {
console.log( a );
a += foo();
console.log( a + b );
如果不是为了这个代码段中的console.log(..)语句(只是作为这个例子中观察副作用的方便形式),JS引擎将会更加自由,如果它想(谁知道它想不想!?),它会重排这段代码:
a = 10 + foo();
b = 30 + c.
多亏JS语义,我们不会观测到看起来很危险的编译器语句重排,但是理解源代码被编写的方式(从上到下)与它在编译后运行的方式之间的联系是多么微弱,依然是很重要的。
编译器语句重排几乎是并发与互动的微型比喻。作为一个一般概念,这样的意识可以帮你更好地理解异步JS代码流问题。
一个JavaScript程序总是被打断为两个或更多的代码块儿,第一个代码块儿 现在 运行,下一个代码块儿 稍后 运行,来响应一个事件。虽然程序是一块儿一块儿地被执行的,但它们都共享相同的程序作用域和状态,所以对状态的每次修改都是在前一个状态之上的。
不论何时有事件要运行,事件轮询 将运行至队列为空。事件轮询的每次迭代称为一个“tick”。用户交互,IO,和定时器会将事件在事件队列中排队。
在任意给定的时刻,一次只有一个队列中的事件可以被处理。当事件执行时,他可以直接或间接地导致一个或更多的后续事件。
并发是当两个或多个事件链条随着事件相互穿插,因此从高层的角度来看,它们在 同时 运行(即便在给定的某一时刻只有一个事件在被处理)。
在这些并发“进程”之间进行某种形式的互动协调通常是有必要的,比如保证顺序或防止“竞合状态”。这些“进程”还可以 协作:通过将它们自己打断为小的代码块儿来允许其他“进程”穿插。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮
被以下专题收入,发现更多相似内容:
如果你是程序员,或者有一颗喜欢写程序的心,喜欢分享技术干货、项目经验、程序员日常?事等等,欢迎投稿《程序员》专题。
专题主编:小...
? 184965人关注
玩转简书的第一步,从这个专题开始。
想上首页热门榜么?好内容想被更多人看到么?来投稿吧!如果被拒也不要灰心哦~入选文章会进一个队...
? 133590人关注
? 11人关注
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
选择支付方式:

参考资料

 

随机推荐