JavaScript非同步程式設計筆記

Hanson_dai發表於2018-10-15

非同步事件的工作方式

事件!事件到底是怎麼工作的?JavaScript出現了多久,對JavaScript非同步事件模型就迷惘了多久。迷惘導致bug,bug導致加班,加班導致沒時間撩妹子,這不是js攻城獅想要的生活。

==為了妹子,一定要理解好JavaScript事件==

JavaScript事件的執行

先來看一個爛大街的面試題

for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 200);
}
// 3 3 3
複製程式碼

為什麼輸出的全都是3??

  1. 只有一個名為i的變數,其作用域由宣告語句var i定義(var定義的i作用城不是迴圈內部,而是擴散至其所在的整個作用域)。
  2. 迴圈結束後,i++還在執行,直到i<3返回false為止。
  3. 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元素時,程式是如何工作的???

  1. 使用者單擊一個已附加有單擊事件處理器的DOM元素時,會有一個單擊事件排入佇列。
  2. 該單擊事件處理器要等到當前所有正在執行的程式碼均已結束後(可能還要等其他此前已排隊的事件也依次結束)才會執行。

恩,用專業點的術語來說,就是事件迴圈,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,會自動重啟掛掉的執行緒

相關文章