淺出 js 非同步事件

不吃貓的魚發表於2017-06-21

JS的單執行緒

如果把主執行緒也看成是一個大塊的函式體的話,單執行緒可以理解成是每次只執行一個函式體。這樣做的好處就是不用考慮變數被多執行緒同時操作而引發的問題。每個程式都有個主執行緒,非同步觸發的事件會被放到事件佇列(event queue)。主執行緒程式執行完畢後,會去事件佇列裡逐個執行佇列裡的handler(因為是單執行緒,所以每次只能處理一個事件,也就是事件之間會相互block)。這個事件輪詢的過程可以表示為

runYourScript(); 
while (atLeastOneEventIsQueued) {
    fireNextQueuedEvent();
};複製程式碼

用setTimeout舉個簡單的例子:

let start = +new Date;
setTimeout(function Task1(){
    let end = +new Date;
    console.log(`Task1: Time elapsed ${end - start} ms`);
}, 500); //500ms後,Task1被放到事件佇列

// single thread
setTimeout(function Task2(){
    let end = +new Date;
    console.log(`Task2: Time elapsed ${end -start} ms`);
    while(+new Date - start < 3000) {} //延遲到3秒後再結束。這裡會block住佇列裡的下一個執行
}, 300); //300ms後,Task2被放到事件佇列

while(+new Date - start < 1000) {} //主執行緒1秒後結束
console.log('main thread ends');
//output: 
//main thread ends
//Task2: Time elapsed 1049 ms
//Task1: Time elapsed 3000 ms複製程式碼

主執行緒裡執行了2個setTimeout方法。setTimeout方法可以理解成把第一個函式引數放到事件佇列裡。所以

  • 300ms後,Task2被放到事件佇列。
  • 500ms後,Task1被放到事件佇列。
  • 1秒後,主執行緒結束,列印出"main thread ends"。
  • 主執行緒結束後會去事件佇列裡輪詢事件。事件佇列裡的第一個事件是Task2。注意這裡Task2列印出來的事件是大於300ms的。這也證明了setTimeout並非是嚴格按照設定的delay時間執行,具體執行時間取決於主執行緒和事件佇列裡的其他事件執行情況。
  • Task2處理完後,事件佇列裡還有個Task1。開始執行Task1。

什麼是非同步函式

JS裡的非同步函式指的是它可以接收一個回撥函式並把該回撥函式放入事件佇列裡,後續執行。因為回撥函式是放到事件佇列裡的,所以非同步函式是非阻塞的。非同步函式可以保證下面這個單元測試永遠通過:

var functionHasReturned = false; 
asyncFunction(() => {
    console.assert(functionHasReturned); 
}); 
functionHasReturned = true;複製程式碼

並非所有接收回撥函式作為引數的函式都是非同步函式。例如Array.prototype.forEach就是同步的。

let before = false;
[1].forEach(() => {
    console.assert(before); 
}); 
before = true;複製程式碼

所以從函式的呼叫是無法看出該函式是否為非同步函式,而應該由函式體的實現進行判斷。有些函式甚至有可能是既可以是非同步的也可以是同步的。

非同步函式的異常捕獲

因為非同步函式的回撥是在事件佇列裡單獨拉出來執行的。所以在非同步函式外面包裹try-catch是無法捕捉到回撥函式裡丟擲的異常的。因為當回撥函式從佇列裡被拉出來執行的時候try-catch所在的程式碼塊已經執行完畢了。

try {
    setTimeout(() => {
        throw new Error('callback error'); 
    }, 0);
} catch (e) {
    console.error('caught callback error');
}
console.log('try-catch block ends');複製程式碼

在上述例子中,當回撥裡的異常被丟擲但沒被捕獲的時候,該異常會直接被主程式所捕獲。在瀏覽器裡可以通過window.onerror,在node裡通過process.uncaughtException可以捕獲此類異常。

處理非同步函式異常的話只能通過回撥函式。例如

let fs = require('fs'); 
fs.readFile('abc.txt', function(err, data) {
    if (err) {
        return console.error(err); 
    }; 
    console.log(data);
});複製程式碼

Reference

Notice

  • 如果您覺得該Repo讓您有所收穫,請「Star 」支援樓主。
  • 如果您想持續關注樓主的最新系列文章,請「Watch」訂閱

相關文章