理解JavaScript中的事件輪詢


原文:http://www.ruanyifeng.com/blog/2014/10/event-loop.html

為什么JavaScript是單線程

JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那么,為什么JavaScript不能有多個線程呢?

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程再某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為准?

所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。

為了利用多核CPU的計算能力,HTML5提出Web Worker標准,運行JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標准並沒有改變JavaScript單線程的本質。

任務隊列

單線程就意味着,所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等着。多線程可以將任務放到不同的線程中去處理。CPU的調度單位是線程,它會在不同的線程之間切換,任務是隸屬於線程的。

如果一個線程中,任務排隊是因為計算量大,CPU忙不過來,倒也算了。但是很多時候CPU處理一個線程時是閑着的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等結果出來,任務才能往下執行。

JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在后面的任務。等到IO設備返回了結果,再回頭,把掛起的任務繼續執行下去。

於是,所有任務可以分成兩種,一種是同步任務,另一種是異步任務。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;異步任務指的是,不進入主線程、而進入“任務隊列”的任務,只有“任務隊列”通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。

注:在理解上面的內容時,不要將CPU處理其他進程(比如QQ等)考慮進來,上面內容是針對CPU處理一個線程來說的。在一個單線程中(比如一個頁面加載過程中的js處理),會包含許多任務。在任務很多的情況下,想要不被阻塞,采用的方案是異步任務+事件輪詢。多線程是將任務分到不同的線程中去,各個線程內部采用的是同步任務。

事件和回調函數

“任務隊列”是事件隊列(也可以理解成消息隊列),表示相關的異步任務可以進入“執行棧”了。主線程讀取“任務隊列”,這個過程是循環不斷的。

“任務隊列”中的事件,除了IO設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入“任務隊列”,等待主線程讀取。

異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。

Event Loop

主線程從“任務隊列”中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為Event Loop(事件輪詢)。

上圖中,主線程運行的時候,產生堆(heap)和棧(stack)。只要棧中的代碼執行完畢,主線程就會去讀取“任務隊列”,依次執行那些事件所對應的回調函數。

任務隊列中的任務,是“回調函數”指定的,然后通過事件觸發添加進去的,是一種異步任務。而在頁面初始化時,主線程執行的任務是同步任務。

需要注意的是,setTimeout()只是將事件插入了"任務隊列",必須等到當前代碼(執行棧)執行完,主線程才會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等很久,所以並沒有辦法保證,回調函數一定會在setTimeout()指定的時間執行。

Node.js的Event Loop

Node.js也是單線程的Event Loop,但是它的運行機制不同於瀏覽器環境。

根據上圖,Node.js的運行機制如下。

  1. V8引擎解析JavaScript腳本
  2. 解析后的代碼,調用Node API
  3. libuv庫負責Node API的執行。它將不同的任務分配給不同的線程,形成一個Event Loop,以異步的方式將任務的執行結果返回給V8引擎。
  4. V8引擎再將結果返回給用戶

除了setTimeout和setInterval這兩個方法,Node.js還提供了另外兩個與“任務隊列”有關的方法:process.nextTick和setImmediate。它們可以幫助我們加深對“任務隊列”的理解。

process.nextTick方法可以在當前“執行棧”的尾部,下一次Event Loop(主線程讀取“任務隊列”)之前,觸發回調函數。也就是說,它指定的任務總是發生在所有一般任務之前。

setImmediate方法則是在當前“任務隊列”的尾部添加事件,也就是說,它指定的任務總是在下一次Event Loop時執行,這與setTimeout很像。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM