JavaScript非同步程式設計筆記
非同步事件的工作方式
事件!事件到底是怎麼工作的?JavaScript出現了多久,對JavaScript非同步事件模型就迷惘了多久。迷惘導致bug,bug導致加班,加班導致沒時間撩妹子,這不是js攻城獅想要的生活。
==為了妹子,一定要理解好JavaScript事件==
JavaScript事件的執行
先來看一個爛大街的面試題
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 200);
}
// 3 3 3
複製程式碼
為什麼輸出的全都是3??
- 只有一個名為i的變數,其作用域由宣告語句var i定義(var定義的i作用城不是迴圈內部,而是擴散至其所在的整個作用域)。
- 迴圈結束後,i++還在執行,直到i<3返回false為止。
- JavaScript事件處理器線上程空閒之前不會執行。
再來看一段程式碼
var start = new Date();
setTimeout(function () {
console.log("回撥觸發間隔1:", new Date() - start, "ms");
}, 500);
setTimeout(function () {
console.log("回撥觸發間隔2:", new Date() - start, "ms");
}, 800);
setTimeout(function () {
console.log("回撥觸發間隔3:" , new Date() - start, "ms");
}, 1100);
while (new Date() - start < 1000) {
}
回撥觸發間隔1: 1002 ms
回撥觸發間隔2: 1003 ms
回撥觸發間隔3: 1101 ms
複製程式碼
最終輸出的毫秒數在不同環境下會有所不同,但是最終數字肯定至少是1000,因為在while迴圈阻塞了執行緒(JavaScript是單執行緒執行),在迴圈結束執行之前,setTimeout的處理器不會被觸發。
為什麼會這樣??
呼叫setTimeout的時候,會有一個延時事件排入佇列,然後setTimeout呼叫之後的程式碼執行,然後之後之後的程式碼執行,然後之後之後之後...
直到再也沒有要執行的程式碼,這個時候佇列事件才會被記起。
如果佇列事件中至少有一個事件適合被觸發(如前面程式碼中的500和800毫秒的延時事件),則JS執行緒會挑選一個事件,並呼叫事件的處理器(回撥函式)。
執行完畢後,回到事件佇列中,繼續下一個...
也就是說:setTimeout 只能保證在指定的時間後將任務(需要執行的函式)插入任務佇列中等候,但是不保證這個任務在什麼時候執行。
大家可以猜想下,使用者單擊一個已附加有單擊事件處理器的DOM元素時,程式是如何工作的???
- 使用者單擊一個已附加有單擊事件處理器的DOM元素時,會有一個單擊事件排入佇列。
- 該單擊事件處理器要等到當前所有正在執行的程式碼均已結束後(可能還要等其他此前已排隊的事件也依次結束)才會執行。
恩,用專業點的術語來說,就是事件迴圈,js不斷的從佇列中迴圈取出處理器執行。
所以,setTimeout(fn,0)只是指定某個任務在主執行緒空閒時,儘可能早得執行。它在"任務佇列"的尾部新增一個事件,因此要等到同步任務和"任務佇列"現有的事件都處理完,才會得到執行。
非同步函式的型別
JavaScript提供的非同步函式分為兩類:I/O函式、計時函式
最為常見的非同步I/O模型是ajax,它是網路IO的一種,在nodejs中最為常見的是檔案IO。
最為常見的非同步計時函式為setTimeout與setInterval,除了前面的示例,這兩個函式還存在一些無法彌補的精度問題。
看下如下兩段程式碼:
var fireCount = 0;
var start = new Date();
var timer = setInterval(function () {
if (new Date() - start > 1000) {
clearInterval(timer);
console.log(fireCount);
return;
}
fireCount++;
},0);
// node環境輸出:860
// chrome環境輸出:252
var fireCount = 0;
var start = new Date();
var flag = true;
while (flag) {
if (new Date() - start > 1000) {
console.log(fireCount);
flag = false;
}
fireCount++;
}
// node環境輸出:4355256
// chrome環境輸出:4515852
複製程式碼
為什麼???
以下資訊引用自網路
事實上HTML5標準規定setTimeout的最短時間間隔是4毫秒;setInterval的最短間隔時間是10毫秒。
在此之前,老版本的瀏覽器都將setTimeout最短間隔設為10毫秒。另外,對於那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常不會立即執行,而是每16.6毫秒執行一次(大多數電腦顯示器的重新整理頻率是60Hz,大概相當於每秒鐘重繪60次,不超過顯示器的重繪頻率,因為即使超過那個頻率使用者體驗也不會有提升)。這時使用requestAnimationFrame()的效果要好於setTimeout()。
nodejs提供了更細粒度的立即非同步執行函式,process.nextTick,setImmediate
瀏覽器提供了一個新的函式requestAnimationFrame函式,它允許以60+幀/秒的速度執行JavaScript動畫;另一方面,它也可以避免後臺選項卡執行這些動畫,節約CPU週期。詳情
console.log是非同步嗎? 在nodejs中是嚴格的同步函式,而在瀏覽器端,則依賴具體瀏覽器的實現,根據測試,基本是同步!!
什麼是非同步函式:函式會導致將來再執行另一個函式,而另一個函式取自於事件佇列(我們一般稱為回撥)。非同步函式一般滿足下面的模式。
var functionHasReturned=false;
asyncFunction(){
console.log(functionHasReturned); // true
}
functionHasReturned=true;
複製程式碼
非同步的錯誤處理
JavaScript中也有try/catch/finally,也存在throw,如果在一次非同步操作中丟擲錯誤,會發生什麼??
下面看兩個《async javascript》書中的例子:
程式碼1:
function getObj(str){
return JSON.parse(str);
}
var obj = getObj("{");
複製程式碼
在node下執行,輸出的錯誤堆疊資訊:
undefined:1
{
SyntaxError: Unexpected end of JSON input
at JSON.parse (<anonymous>)
at getObj (/home/xingmu/ws/practice/myapp/test/test.js:2:14)
at Object.<anonymous> (/home/xingmu/ws/practice/myapp/test/test.js:4:11)
at Module._compile (module.js:652:30)
at Object.Module._extensions..js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)
at Function.Module.runMain (module.js:693:10)
at startup (bootstrap_node.js:188:16)
複製程式碼
程式碼2:
setTimeout(function a(){
setTimeout(function b(){
setTimeout(function c(){
throw new Error("我犯錯誤了,快來抓我!");
},0);
},0);
},0);
複製程式碼
輸出:
/home/xingmu/ws/practice/myapp/test/test.js:4
throw new Error("我犯錯誤了,快來抓我!");
^
Error: 我犯錯誤了,快來抓我!
at Timeout.c [as _onTimeout] (/home/xingmu/ws/practice/myapp/test/test.js:4:10)
at ontimeout (timers.js:482:11)
at tryOnTimeout (timers.js:317:5)
at Timer.listOnTimeout (timers.js:277:5)
複製程式碼
為什麼程式碼2輸出的錯誤堆疊資訊只有c ?
因為在執行時,c是從佇列中取出來的,而這個時候a和b還在佇列中,並不知道c執行出錯了。
下面再看一段程式碼:
try{
setTimeout(function(){
throw new Error("我犯錯誤了,快來抓我!");
},0);
}catch(e){
console.log(e);
console.log("抓到你了!");
}finally{
console.log("我是終結者!");
}
複製程式碼
輸出資訊:
我是終結者!
/home/xingmu/ws/practice/myapp/test/test.js:3
throw new Error("我犯錯誤了,快來抓我!");
^
Error: 我犯錯誤了,快來抓我!
at Timeout._onTimeout (/home/xingmu/ws/practice/myapp/test/test.js:3:9)
at ontimeout (timers.js:482:11)
at tryOnTimeout (timers.js:317:5)
at Timer.listOnTimeout (timers.js:277:5)
複製程式碼
從這裡可以看出,try/catch塊只會捕獲setTimeout函式自身內部發生的錯誤,而setTimeout的回撥是非同步執行的,即使丟擲錯誤,也無法捕獲。
所以說對非同步執行的函式,使用try/catch塊並不能達到我們想要的效果, 那麼對於非同步回撥的錯誤該怎麼處理呢??
下面來看下,在nodejs的API中比較常見的錯誤處理模式:
var fs = require("fs");
fs.readFile("abc.text", function (err, data) {
if (err) {
console.log(err);
return;
}
console.log(data.toString("utf8"));
});
// { Error: ENOENT: no such file or directory, open 'abc.text' errno: -2, code: 'ENOENT', syscall: 'open', path: 'abc.text' }
複製程式碼
在nodejs中,類似這樣的API非常多,在回撥函式中,第一個引數總是接收一個錯誤,這樣就可以讓回撥函式自己決定怎麼處理這個錯誤。
而在瀏覽器中,我們最熟悉的回撥錯誤處理模式是像jquery中的ajax一樣,針對成功和失敗,各定義一個單獨的回撥:
$.ajax({
type:'POST',
url:'/data',
data: $('form').serialize(),
success:function(response,status,xhr){
//dosomething...
},
error:function (textStatus) {//請求失敗後呼叫的函式
//dosomething...
}
});
複製程式碼
不管是那個一個執行環境,對於非同步的錯誤處理有一點是一致的: 只能在回撥的內部處理源於回撥的錯誤。
未捕獲異常的處理
是的,總會有意想不到的錯誤發生,這時候該怎麼處理??
- 瀏覽器環境中,我們經常可以在瀏覽器控制檯看到很多未捕獲的錯誤資訊,在開發環境這些資訊可以幫助我們除錯,如果想修改這種行為,可以給window.error新增一個處理器,用來全域性處理未捕獲異常。
window.onerror = function(error){
// do something
// 比如向伺服器報告出現的未捕獲異常
// 比如給使用者統一的訊息處理
// return true; 返回true,可以阻止瀏覽器的預設行為,徹底忽略所有的錯誤
}
複製程式碼
看一段示例程式碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
try {
setTimeout(function () {
throw new Error("我犯錯誤了,快來抓我!");
}, 0);
} catch (e) {
console.log(e);
console.log("抓到你了!");
} finally {
console.log("我是終結者!");
}
window.onerror = function (error) {
alert("頁面出錯了");
// do something other
return true;
};
</script>
</body>
</html>
複製程式碼
- node環境中,有domain和process.onuncaughtexception兩種方式來處理未捕獲異常,但是後端的處理比較複雜,javascript作為一個單執行緒程式,對於異常的處理更要慎重。
恩,意思就是我也沒有最好的方案。。。
當然很多工具也可以幫我們簡化處理,比如pm2,會自動重啟掛掉的執行緒