本文深入解析JavaScript异步编程机制,对比setTimeout与其他异步API的性能差异,指出零延迟setTimeout的实际开销。重点介绍基于Python Twisted框架衍生的Deferred类,通过链式调用管理成功与失败回调,并演示如何利用该模式处理AJAX请求、捕获异步错误及实现非阻塞的队列循环执行。
异步编程基础与setTimeout性能分析
异步编程的概念可以类比为排队场景:当队伍前方有人突然离开(如去洗手间)导致阻塞时,后续人员需要被允许绕过障碍继续前进。在Web开发中,AJAX技术正是体现了这一思想:尽管网络请求仍在进行中,用户界面和其他操作仍可继续执行,无需等待响应结果。
在JavaScript中实现异步功能主要依赖BOM提供的setTimeout函数,但DOM层面也提供了多种异步机制。例如,XMLHttpRequest对象及其onreadystatechange回调、script标签的onreadystatechange事件、image的onload与onerror事件、iframe的onload事件、DOM元素的事件监听、HTML5的postMessage跨域通信,以及QuickTime和Flash对象的加载回调等,均支持异步操作。
过去国内曾广泛推崇setTimeout零延迟调用的技巧,但实际上它是所有延迟方式中耗时最长的,最低耗时约10多毫秒。若使用setTimeout开发动画特效,会导致运行缓慢。以下是一个性能测试案例:
<!doctype html> <html lang="en"> <head> <title>Async performance test</title> <meta charset="gb2312"> <style> pre { background-color: #eeeeee; padding: 20px; } div.test { margin: 20px; padding: 10px; border: solid black 1px; } </style> </head> <body> <div class="test"> <h3>setTimeout (slow, takes about 10 sec)</h3> <pre class="source"> function async(callback) { setTimeout(callback, 0); } </pre> <p> <input type="button" value="run" onclick="runtest(this.parentNode.parentNode)"> </p> </div> <div class="test"> <h3>img.onerror (data:uri)</h3> <pre class="source"> function async(callback) { var img = new Image; img.addEventListener('error', callback, false); img.src = 'data:,foo'; } </pre> <p> <input type="button" value="run" onclick="runtest(this.parentNode.parentNode)"> </p> </div> <div class="test"> <h3>script.onreadystatechange</h3> <pre class="source"> function async(callback) { var script = document.createElement("script"); script.type = "text/javascript"; script.src = "javascript:"; script.onreadystatechange = function () { document.body.removeChild(script); callback(); } document.body.appendChild(script); } </pre> <p> <input type="button" value="run" onclick="runtest(this.parentNode.parentNode)"> </p> </div> <div class="test"> <h3>script.onload (data:uri)</h3> <pre class="source"> function async(callback) { var script = document.createElement('script'); script.onload = function() { document.body.removeChild(script); callback(); } script.src = 'data:text/javascript,'; document.body.appendChild(script); } </pre> <p> <input type="button" value="run" onclick="runtest(this.parentNode.parentNode)"> </p> </div> <div class="test"> <h3>xhr.onreadystatechange (data:text/plain,foo)</h3> <pre class="source"> function async(callback) { var xhr = new XMLHttpRequest; xhr.open('GET','data:text/plain,foo',true); xhr.onreadystatechange = function() { xhr.onreadystatechange = null; callback(); }; xhr.send(null); } </pre> <p> <input type="button" value="run" onclick="runtest(this.parentNode.parentNode)"> </p> </div> <div class="test"> <h3>self.postMessage</h3> <pre class="source"> function async(callback) { var n = ++async.count; window.addEventListener('message',function(e){ if (e.data == n) { window.removeEventListener('message', arguments.callee,false); callback(); } },false); window.postMessage(n, location.protocol + "//" + location.host); } async.count = 0; </pre> <p> <input type="button" value="run" onclick="runtest(this.parentNode.parentNode)"> </p> </div> </body> </html> [Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]
| Chromium | Safari | Firefox | Opera 10.10 | Opera 10.50 | |
|---|---|---|---|---|---|
| setTimeout | 4.32ms | 10.201ms | 10.302ms | 10.38ms | 9.876ms |
| img.onerror | 0.199ms | 0.678ms | 0.201ms | 0.058ms | 0.575ms |
| script.onreadystatechange | fail | fail | fail | fail | fail |
| script.onload | 0.414ms | 0.138ms | 0.414ms | fail | fail |
| xhr.onreadystatechange | fail | 0.622ms | fail | 0.078ms | 0.079ms |
| self.postMessage | 0.096ms | 0.123ms | 0.112ms | 0.049ms | 0.094ms |
Deferred模式与AJAX回调处理
为了解决异步调用复杂性,Mochikit借鉴了Python Twisted框架中的Deferred类,用于处理AJAX回调。AJAX回调通常包含两种状态:成功加载时的回调和请求失败时的回调。例如,IE8的XDomainRequest对象就具备这两种回调,标准浏览器中的script和image元素也分别通过onload和onerror实现。Mochikit的Deferred实例内部维护一个数组,依次存储并执行这些回调。这一设计后来被dojo库发扬光大,具体用法可参考相关文档。
以下是我在框架中对Deferred的应用示例,已将其整合至个人框架中:
代码如下:
<!doctype html>
<html>
<head>
<title>异步操作例子 by 司徒正美</title>
<meta charset="utf-8"/>
<meta content="IE=8" http-equiv="X-UA-Compatible"/>
<meta name="keywords" content="异步操作例子 by 司徒正美" />
<meta name="description" content="异步操作例子 by 司徒正美" />
<%= javascript_include_tag "dom.js" %>
<%= javascript_tag "window._token = '#{form_authenticity_token}'" if ActionController::Base.allow_forgery_protection %>
<script type="text/javascript" charset="utf-8">
dom.ready(function(){
dom.require("ajax");
dom.ajax({method:"post",
async:true,
dataType:"text",
data:{authenticity_token:window._token}
}).next(function(a){
alert(a)
});
dom.jsonp({url:"http://del.icio.us/feeds/json/fans/stomita"}).next(function(json){
alert(json)
}).error(function(e){
alert(e)
});
});
</script>
</head>
<body>
</body>
</html> 后台:
代码如下:
class HomeController < ApplicationController
def index
if request.xhr?
name = params[:name]
puts "-------------"
render :text => "<p>The time is <b>" + DateTime.now.to_s + "</b></p>"
end
end
end 异步错误捕获与队列执行机制
在日本一博客提到这样一个捕捉异步错误的例子:
代码如下:
function throwError(){
throw new Error('ERROR');
}
try{
setTimeout(throwError, 3000);
} catch(e){
alert(e);
} 看来try...catch是无法捕捉这种形式的错误,window.onerror可以,但好像只有IE与FF支持。如果用Deferred来处理,就简单了!
代码如下:
dom.Deferred.next(function () {
throw new Error("错误")
}).wait(1).error(function(e){
alert(e instanceof Error)
}); 列队处理。由于是使用了异步,因此不会阻塞页面的演染。
代码如下:
<!doctype html>
<html>
<head>
<title>异步操作例子 by 司徒正美</title>
<meta charset="utf-8"/>
<meta content="IE=8" http-equiv="X-UA-Compatible"/>
<meta name="keywords" content="异步操作例子 by 司徒正美" />
<meta name="description" content="异步操作例子 by 司徒正美" />
<%= javascript_include_tag "dom.js" %>
<script type="text/javascript" charset="utf-8">
dom.require("deferred");
dom.require("query");
dom.ready(function(){
var a = dom("#aaa")[0];
dom.Deferred.loop(10,function(i){
a.innerHTML += i+"<br/>"
});
dom.Deferred.loop(10,function(i){
a.innerHTML += String.fromCharCode(i+97)+"<br/>"
});
dom.Deferred.loop(10,function(i){
a.innerHTML += "司徒正美"+i+"<br/>"
});
});
/*结果
0
a
司徒正美0
1
司徒正美1
2
c
司徒正美2
3
d
司徒正美3
4
e
司徒正美4
5
f
司徒正美5
6
g
司徒正美6
7
h
司徒正美7
8
i
司徒正美8
9
j
司徒正美9
*/
</script>
</head>
<body>
<div id="aaa"></div>
</body>
</html>