標籤:cloud 互動 那是 事件 程式 初始化 images can src
隨著對JavaScript學習的深入和實踐經驗的積累,一些原理和底層的東西也開始逐漸瞭解。早先也看過一些關於js單線程和事件迴圈的文章,不過當時看的似懂非懂,只留了一個大概的印象:瀏覽器中的js程式時是單線程的。嗯,就這麼點印象。當時也有些疑問:既然是單線程的,那非同步呼叫是怎麼實現的?計時器是靠誰來計時的,這單線程總不能一邊執行程式一邊計時吧?那些耗時的I/O操作為啥沒把線程阻塞,不是說好的單線程嗎?相信很多不瞭解JavaScript單線程的同學也有過類似的疑問。
今天看了不少相關的資料,就詳細地總結一下,希望能幫到大家,同時自己也梳理一下知識點,理解是一回事,能寫下來是另一回事。好了,開始本文。
一、JavaScript單線程
在瀏覽器的一個頁面中,該頁面的JS程式只有一個線程,故曰單線程。因為是單線程,所以程式的執行順序就是從上到下依次執行,同一時間內只能有一段代碼被執行。那為什麼不用多線程,這樣不是更能充分利用CPU,提高效率嗎?
早期的網頁內容非常簡單,單線程足以應付,所以在設計之初,我估計設計者就沒考慮使用多線程。另外,JavaScript主要用來處理使用者與頁面產生的互動,以及操作DOM;如果以多線程的方式來操作DOM,一個線程要求刪除該DOM,另一個要求修改DOM樣式,那麼瀏覽器該聽誰的?這大大增加了程式設計的複雜度,本來前端都不好學,不是麼~~~簡簡單單多好。
雖然JavaScript是單線程的,可是瀏覽器內部不是單線程的。你的一些I/O操作、定時器的計時和事件監聽(click, keydown...)等都是由瀏覽器提供的其他線程來完成的。
如果想利用多執行緒一些耗時較長的任務,可以使用HTML5提出的Web Worker。
二、任務隊列和事件迴圈
提到非同步機制,不得不說到任務隊列和事件迴圈,這裡理解JS非同步機制的要點。
首先理解瀏覽器的並行存取模型,來看下面這個圖:
左邊的棧儲存的是同步任務,所謂同步的任務就是那些能立即執行、不耗時的任務,如變數和函數的初始化、事件的綁定等等那些不需要回呼函數的操作都可歸為這一類。右邊的堆用來儲存聲明的變數、對象。下面的隊列就是任務隊列,一旦某個非同步任務有了響應就會被推入隊列中。如使用者的點擊事件、瀏覽器收到服務的響應和後面提到的setTimeout插入的事件。每個非同步任務都和一個回呼函數相關聯。
一個js程式的單線程用來執行棧中的同步任務,當所有同步任務執行完畢後,棧被清空,然後讀取任務隊列中的一個待處理任務,並把相關回呼函數壓入棧中,單線程開始執行新的同步任務,執行完畢。
單線程從任務隊列中讀取任務是不斷迴圈的,每次棧被清空後,都會在任務隊列中讀取新的任務,如果沒有新的任務,就會等待,直到有新的任務,這就叫任務迴圈。因為每個任務都由一個事件所觸發,所以也叫事件迴圈。
三、非同步機制
有了上面兩節做鋪墊,理解非同步機制就容易多了。拿ajax來說,當頁面的單線程執行xhr.send()之後,對於頁面來說發送任務已經完成了。怎麼發送,那是瀏覽器的事,和單線程無關;什麼時候響應,這事說不準。為了及時地得到響應的內容,在單線程中註冊相應的事件就好xhr.onreadystatechange = fn() {...}。註冊之後,瀏覽器會在內部的其他線程中自動地幫我們監聽該事件。直到該事件被觸發,瀏覽器會在任務隊列中添加一個任務等待該單線程執行。
四、定時器
setTimeout的作用是在間隔一定的時間後,將回呼函數插入任務隊列中,等棧中的同步任務都執行完畢後,再執行。因為棧中的同步任務也會耗時,所以間隔的時間一般會大於等於指定的時間。
setTimeout(fn, 0)的意思是,將回呼函數fn立刻插入任務隊列,等待執行,而不是立即執行。看一個例子:
setTimeout(function() { console.log("a")}, 0)for(let i=0; i<10000; i++) {}console.log("b")// 結果:b a
列印結果表明回呼函數並沒有立刻執行,而是等待棧中的任務執行完畢後才執行的。棧中的任務執行多久,它就得等多久。
五、總結
JavaScript單線程和其非同步機制就如上所述。所謂的單線程並不孤單,它的背後有瀏覽器的其他線程為其服務,其非同步也得靠其他線程來監聽事件的響應,並將回呼函數推入到任務隊列等待執行。單線程所做的就是執行棧中的同步任務,執行完畢後,再從任務隊列中取出一個事件(沒有事件的話,就等待事件),然後開始執行棧中相關的同步任務,不斷的這樣迴圈。
參考資料
- 並行存取模型與Event Loop
- JavaScript 運行機制詳解:再談Event Loop
- JavaScript非同步機制
JavaScript單線程和非同步機制