JavaScript异步编程:从回调到Deferred模式详解

作者:木棉时间:2026-10-24 23:18:30浏览:237

本文深入解析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>
作者最新文章