JS異步那些事 一 (基礎(chǔ)知識(shí))
JS異步那些事 二 (分布式事件)
JS異步那些事 三 (Promise)
JS異步那些事 四(HTML 5 Web Workers)
JS異步那些事 五 (異步腳本加載)
js事件概念
異步回調(diào):
首先了講講js中 兩個(gè)方法 setTimeout()和 setInterval()
定義和用法:
setTimeout() 方法用于在指定的毫秒數(shù)后調(diào)用函數(shù)或計(jì)算表達(dá)式锌唾。
語(yǔ)法:
setTimeout(callback,time)
callback 必需码秉。要調(diào)用的函數(shù)后要執(zhí)行的 JavaScript 代碼串啦吧。
time 必需循衰。在執(zhí)行代碼前需等待的毫秒數(shù)悲酷。
setInterval() 方法和setTimeout很相似嘶卧,可按照指定的周期(以毫秒計(jì))來(lái)調(diào)用函數(shù)或計(jì)算表達(dá)式言询。
<script type="text/javascript">
function timeCount()
{console.log("this is setTimeout");
setTimeout("timeCount()",1000);
}
function timeCount2(){
console.log("this is setInterval");
}
setInterval("timeCount2()",1000);
timeCount();
timeCount2();
</script>
比如上述代碼就是可以每隔1000毫秒延遲執(zhí)行timecount函數(shù)俯萎,不同的是后者是周期的執(zhí)行timecount函數(shù),
SetInterval為自動(dòng)重復(fù)运杭,setTimeout不會(huì)重復(fù)夫啊。
線程阻塞
JavaScript引擎是單線程運(yùn)行的,瀏覽器無(wú)論在什么時(shí)候都只且只有一個(gè)線程在運(yùn)行JavaScript程序.
<script type="text/javascript">
function f() { console.log("hello world");}
var t = new Date(); //運(yùn)行5秒
while(true) {
if(new Date() - t > 5000) {
break; }
}
setTimeout(f, 1000);
</script>
執(zhí)行上述代碼,可以發(fā)現(xiàn)辆憔,總的運(yùn)行時(shí)間幾乎要6秒多撇眯,因?yàn)槭菃尉€程,會(huì)在while循環(huán)里面消耗5秒的時(shí)間虱咧,然后才去執(zhí)行settimeout函數(shù)熊榛。
隊(duì)列
瀏覽器是基于一個(gè)事件循環(huán)的模型,在這里面彤钟,可以有多個(gè)任務(wù)隊(duì)列来候,比如render是一個(gè)隊(duì)列,響應(yīng)用戶輸入是一個(gè)逸雹,script執(zhí)行是一個(gè)营搅。任務(wù)隊(duì)列里放的是任務(wù),同一個(gè)任務(wù)來(lái)源的任務(wù)肯定在同一個(gè)任務(wù)隊(duì)列里梆砸。任務(wù)有優(yōu)先級(jí)转质,鼠標(biāo)或鍵盤響應(yīng)事件優(yōu)先級(jí)高,大概是其他任務(wù)的3倍帖世。
而我們常用的setTimeout函數(shù)休蟹,其本質(zhì)上也就是向這個(gè)任務(wù)隊(duì)列添加回調(diào)函數(shù),JavaScript引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來(lái).由于單線程關(guān)系,這些任務(wù)得進(jìn)行排隊(duì),一個(gè)接著一個(gè)被引擎處理.
如果隊(duì)列非空,引擎就從隊(duì)列頭取出一個(gè)任務(wù),直到該任務(wù)處理完,即返回后引擎接著運(yùn)行下一個(gè)任務(wù),在任務(wù)沒(méi)返回前隊(duì)列中的其它任務(wù)是沒(méi)法被執(zhí)行的.
異步函數(shù)類型
異步IO:
首先來(lái)看看很典型的一個(gè)例子 ajax
<script type="text/javascript">
var ajax = new XMLHttpRequest;
ajax.open("GET",url);
ajax.send(null);
ajax.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.status === 200) {
return success(request.responseText);
} else {
return fail(request.status);
}
}
}
</script>
異步計(jì)時(shí):
setTimeout,setInterval都是基于事件驅(qū)動(dòng)型的赂弓,通常瀏覽器不會(huì)給這個(gè)太快的速度绑榴,一般是200次/秒,效率太低了是吧如果遇到有密集型的運(yùn)算的話盈魁,那就呵呵了翔怎。但是在node.js中還有process.nextTick()這個(gè)強(qiáng)大的東西,運(yùn)行的速度將近10萬(wàn)次/秒杨耙,很可觀赤套。
process.nextTick(callback)
功能:在事件循環(huán)的下一次循環(huán)中調(diào)用 callback 回調(diào)函數(shù)。效果是將一個(gè)函數(shù)推遲到代碼書寫的下一個(gè)同步方法執(zhí)行完畢時(shí)或異步方法的事件回調(diào)函數(shù)開始執(zhí)行時(shí)珊膜;與setTimeout(fn, 0) 函數(shù)的功能類似容握,但它的效率高多了。
基于node.js的事件循環(huán)分析车柠,每一次循環(huán)就是一次tick剔氏,每一次tick時(shí),v8引擎從事件隊(duì)列中取出所有事件依次進(jìn)行處理堪遂,如果遇到nextTick事件介蛉,則將其加入到事件隊(duì)尾,等待下一次tick到來(lái)時(shí)執(zhí)行溶褪;造成的結(jié)果是币旧,nextTick事件被延遲執(zhí)行;
nextTick的確是把某任務(wù)放在隊(duì)列的最后(array.push)
nodejs在執(zhí)行任務(wù)時(shí)猿妈,會(huì)一次性把隊(duì)列中所有任務(wù)都拿出來(lái)吹菱,依次執(zhí)行
如果全部順利完成,則刪除剛才取出的所有任務(wù)彭则,等待下一次執(zhí)行
如果中途出錯(cuò)鳍刷,則刪除已經(jīng)完成的任務(wù)和出錯(cuò)的任務(wù),等待下次執(zhí)行
如果第一個(gè)就出錯(cuò)俯抖,則throw error
下面看一下應(yīng)用場(chǎng)景(包含計(jì)算密集型操作输瓜,將其進(jìn)行遞歸處理,而不阻塞進(jìn)程):
var http = require('http');
var wait = function (mils) {
var now = new Date;
while (new Date - now <= mils);
};
function compute() {
// performs complicated calculations continuously
console.log('start computing');
wait(1000);
console.log('working for 1s, nexttick');
process.nextTick(compute);
}
http.createServer(function (req, res) {
console.log('new request');
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
}).listen(5000, '127.0.0.1');
compute();
異步錯(cuò)誤處理
異步異常的特點(diǎn)
由于js的回調(diào)異步特性芬萍,無(wú)法通過(guò)try catch來(lái)捕捉所有的異常:
try {
process.nextTick(function () {
foo.bar();
});
} catch (err) {
//can not catch it
}
而對(duì)于web服務(wù)而言尤揣,其實(shí)是非常希望這樣的:
//express風(fēng)格的路由
app.get('/index', function (req, res) {
try {
//業(yè)務(wù)邏輯
} catch (err) {
logger.error(err);
res.statusCode = 500;
return res.json({success: false, message: '服務(wù)器異常'});
}
});
如果try catch能夠捕獲所有的異常,這樣我們可以在代碼出現(xiàn)一些非預(yù)期的錯(cuò)誤時(shí)柬祠,能夠記錄下錯(cuò)誤的同時(shí)北戏,友好的給調(diào)用者返回一個(gè)500錯(cuò)誤÷祝可惜嗜愈,try catch無(wú)法捕獲異步中的異常旧蛾。
難道我們就這樣放棄了么? 其實(shí)還有一個(gè)辦法
onerror事件
我們一般通過(guò)函數(shù)名傳遞的方式(引用的方式)將要執(zhí)行的操作函數(shù)傳遞給onerror事件蠕嫁,如
window.onerror=reportError;
window.onerror=function(){alert('error')}
但我們可能不知道該事件觸發(fā)時(shí)還帶有三個(gè)默認(rèn)的參數(shù)锨天,他們分別是錯(cuò)誤信息,錯(cuò)誤頁(yè)面的url和錯(cuò)誤行號(hào)拌阴。
<script type="text/javascript">
window.onerror=testError;
function testError(){
arglen=arguments.length;
var errorMsg="參數(shù)個(gè)數(shù):"+arglen+"個(gè)";
for(var i=0;i<arglen;i++){
errorMsg+="\n參數(shù)"+(i+1)+":"+arguments[i];
}
alert(errorMsg);
window.onerror=null;
return true;
}
function test(){
error
}
test()
</script>
嵌套式回調(diào)的解嵌套
JavaScript中最常見的反模式做法是绍绘,回調(diào)內(nèi)部再嵌套回調(diào)奶镶。
<script type="text/javascript">
function checkPassword(username, passwordGuess, callback) {
var queryStr = 'SELECT * FROM user WHERE username = ?';
db.query(queryStr, username, function (err, result) {
if (err) throw err;
hash(passwordGuess, function(passwordGuessHash) {
callback(passwordGuessHash === result['password_hash']);
});
});
} </script>
這里定義了一個(gè)異步函數(shù)checkPassword迟赃,它觸發(fā)了另一個(gè)異步函數(shù)db.query,而后者又可能觸發(fā)另外一個(gè)異步函數(shù)hash厂镇。它能用纤壁,而且簡(jiǎn)潔明了。但是捺信,如果試圖向其添加新特性酌媒,它就會(huì)變得毛里毛躁、險(xiǎn)象環(huán)生迄靠,比如去處理那個(gè)數(shù)據(jù)庫(kù)錯(cuò)誤秒咨,而不是拋出錯(cuò)誤、記錄嘗試訪問(wèn)數(shù)據(jù)庫(kù)的次數(shù)掌挚、阻塞訪問(wèn)數(shù)據(jù)庫(kù)雨席,等等。
下面我們換一種寫法吠式,雖然這種寫法很啰嗦但是可讀性更高而且更易擴(kuò)展陡厘。
<script type="text/javascript">
function checkPassword(username, passwordGuess, callback) {
var passwordHash;
var queryStr = 'SELECT * FROM user WHERE username = ?';
db.query(qyeryStr, username, queryCallback);
function queryCallback(err, result) {
if (err) throw err;
passwordHash = result['password_hash'];
hash(passwordGuess, hashCallback);
}
function hashCallback(passwordGuessHash) {
callback(passwordHash === passwordGuessHash);
}
} </script>
在平時(shí)寫嵌套時(shí),我們應(yīng)該盡量避免多層嵌套特占,不然中間某個(gè)地方出錯(cuò)了將會(huì)導(dǎo)致你投入更多的時(shí)間去debug糙置。