JS是單線程,你瞭解其運行機制嗎?

一. 區分進程和線程

不少新手是區分不清線程和進程的,沒有關係。這很正常。先看看下面這個形象的比喻:html

進程是一個工廠,工廠有它的獨立資源-工廠之間相互獨立-線程是工廠中的工人,多個工人協做完成任務-工廠內有一個或多個工人-工人之間共享空間

若是是windows電腦中,能夠打開任務管理器,能夠看到有一個後臺進程列表。對,那裏就是查看進程的地方,並且能夠看到每一個進程的內存資源信息以及cpu佔有率。vue

image

因此,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)java

最後,再用較爲官方的術語描述一遍:node

  • 進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
  • 線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)

提示:react

  • 不一樣進程之間也能夠通訊,不過代價較大
  • 如今,通常通用的叫法:單線程與多線程,都是指在一個進程內的單和多。(因此核心仍是得屬於一個進程才行)

二. 瀏覽器是多進程的

理解了進程與線程了區別後,接下來對瀏覽器進行必定程度上的認識:(先看下簡化理解)web

  • 瀏覽器是多進程的
  • 瀏覽器之因此可以運行,是由於系統給它的進程分配了資源(cpu、內存)
  • 簡單點理解,每打開一個Tab頁,就至關於建立了一個獨立的瀏覽器進程。

關於以上幾點的驗證,請再第一張圖:ajax

image

圖中打開了Chrome瀏覽器的多個標籤頁,而後能夠在Chrome的任務管理器中看到有多個進程(分別是每個Tab頁面有一個獨立的進程,以及一個主進程)。segmentfault

感興趣的能夠自行嘗試下,若是再多打開一個Tab頁,進程正常會+1以上(不過,某些版本的ie倒是單進程的)

注意:在這裏瀏覽器應該也有本身的優化機制,有時候打開多個tab頁後,能夠在Chrome任務管理器中看到,有些進程被合併了(因此每個Tab標籤對應一個進程並不必定是絕對的)windows

3、爲何JavaScript是單線程?

JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。api

JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?

因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。

爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。

四. JavaScript是單線程,怎樣執行異步的代碼?

單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。
js引擎執行異步代碼而不用等待,是因有爲有 消息隊列和事件循環。

消息隊列:消息隊列是一個先進先出的隊列,它裏面存放着各類消息。
事件循環:事件循環是指主線程重複從消息隊列中取消息、執行的過程。

實際上,主線程只會作一件事情,就是從消息隊列裏面取消息、執行消息,再取消息、再執行。當消息隊列爲空時,就會等待直到消息隊列變成非空。並且主線程只有在將當前的消息執行完成後,纔會去取下一個消息。這種機制就叫作事件循環機制,取一個消息並執行的過程叫作一次循環。

事件循環用代碼表示大概是這樣的:

while(true) {
    var message = queue.get();
    execute(message);
}

那麼,消息隊列中放的消息具體是什麼東西?消息的具體結構固然跟具體的實現有關,可是爲了簡單起見,咱們能夠認爲:

消息就是註冊異步任務時添加的回調函數。

再次以異步AJAX爲例,假設存在以下的代碼:

$.ajax('http://segmentfault.com', function(resp) {
    console.log('我是響應:', resp);
});

// 其餘代碼
...
...
...

主線程在發起AJAX請求後,會繼續執行其餘代碼。AJAX線程負責請求segmentfault.com,拿到響應後,它會把響應封裝成一個JavaScript對象,而後構造一條消息:

// 消息隊列中的消息就長這個樣子
var message = function () {
    callbackFn(response);
}

其中的callbackFn就是前面代碼中獲得成功響應時的回調函數。

主線程在執行完當前循環中的全部代碼後,就會到消息隊列取出這條消息(也就是message函數),並執行它。到此爲止,就完成了工做線程對主線程的通知,回調函數也就獲得了執行。若是一開始主線程就沒有提供回調函數,AJAX線程在收到HTTP響應後,也就不必通知主線程,從而也不必往消息隊列放消息。

用圖表示這個過程就是:

image

從上文中咱們也能夠獲得這樣一個明顯的結論,就是:

異步過程的回調函數,必定不在當前這一輪事件循環中執行。

事件循環進階:macrotask與microtask

一張圖展現JavaScript中的事件循環:

image

一次事件循環:先運行macroTask隊列中的一個,而後運行microTask隊列中的全部任務。接着開始下一次循環(只是針對macroTask和microTask,一次完整的事件循環會比這個複雜的多)。

JS中分爲兩種任務類型:macrotask和microtask,在ECMAScript中,microtask稱爲jobs,macrotask可稱爲task

它們的定義?區別?簡單點能夠按以下理解:

macrotask(又稱之爲宏任務),能夠理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)

每個task會從頭至尾將這個任務執行完畢,不會執行其它

瀏覽器爲了可以使得JS內部task與DOM任務可以有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行從新渲染
(task->渲染->task->...)

microtask(又稱爲微任務),能夠理解是在當前 task 執行結束後當即執行的任務

也就是說,在當前task任務後,下一個task以前,在渲染以前

因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染

也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前)

分別很麼樣的場景會造成macrotask和microtask呢?

macroTask: 主代碼塊, setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering(能夠看到,事件隊列中的每個事件都是一個macrotask)

microTask: process.nextTick, Promise, Object.observe, MutationObserver

補充:在node環境下,process.nextTick的優先級高於Promise,也就是能夠簡單理解爲:在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,而後纔會執行微任務中的Promise部分。

另外,setImmediate則是規定:在下一次Event Loop(宏任務)時觸發(因此它是屬於優先級較高的宏任務),(Node.js文檔中稱,setImmediate指定的回調函數,老是排在setTimeout前面),因此setImmediate若是嵌套的話,是須要通過多個Loop才能完成的,而不會像process.nextTick同樣沒完沒了。

實踐:上代碼

咱們以setTimeout、process.nextTick、promise爲例直觀感覺下兩種任務隊列的運行方式。

console.log('main1');

process.nextTick(function() {
    console.log('process.nextTick1');
});

setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);

new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});

console.log('main2');

彆着急看答案,先以上面的理論本身想一想,運行結果會是啥?

最終結果是這樣的:

main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

process.nextTick 和 promise then在 setTimeout 前面輸出,已經證實了macroTask和microTask的執行順序。可是有一點必需要指出的是。上面的圖容易給人一個錯覺,就是主進程的代碼執行以後,會先調用macroTask,再調用microTask,這樣在第一個循環裏必定是macroTask在前,microTask在後。

可是最終的實踐證實:在第一個循環裏,process.nextTick1和promise then這兩個microTask是在setTimeout這個macroTask裏以前輸出的,這是爲何呢?

由於主進程的代碼也屬於macroTask(這一點我比較疑惑的是主進程都是一些同步代碼,而macroTask和microTask包含的都是一些異步任務,爲啥主進程的代碼會被劃分爲macroTask,不過從實踐來看確實是這樣,並且也有理論支撐:【翻譯】Promises/A+規範)。

主進程這個macroTask(也就是main一、promise和main2)執行完了,天然會去執行process.nextTick1和promise then這兩個microTask。這是第一個循環。以後的setTimeout和process.nextTick2屬於第二個循環

別看上面那段代碼好像特別繞,把原理弄清楚了,都同樣 ~

requestAnimationFrame、Object.observe(已廢棄) 和 MutationObserver這三個任務的運行機制你們能夠從上面看到,不一樣的只是具體用法不一樣。重點說下UI rendering。在HTML規範:event-loop-processing-model裏敘述了一次事件循環的處理過程,在處理了macroTask和microTask以後,會進行一次Update the rendering,其中細節比較多,總的來講會進行一次UI的從新渲染。

事件循環機制進一步補充

這裏就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I’m stuck in an event-loop》)

image

上圖大體描述就是:

  • 主線程運行時會產生執行棧,棧中的代碼調用某些api時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax請求完畢)
  • 而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調
  • 如此循環
  • 注意,老是要等待棧中的代碼執行完畢後纔會去讀取事件隊列中的事件

五. 最後

看到這裏,應該對JS的運行機制有必定的理解了吧。

參考:

  1. http://www.ruanyifeng.com/blo...
  2. https://mp.weixin.qq.com/s/vI...
  3. https://mp.weixin.qq.com/s?__...
  4. https://mp.weixin.qq.com/s/k_...

我不是大神,也不是什麼牛人,寫這個號的目的是爲了記錄我自學 web全棧 的筆記。

全棧修煉 有興趣的朋友能夠掃下方二維碼關注個人公衆號

我會不按期更新有價值的內容,長期運營。

關注公衆號並回復 福利 可領取免費學習資料,福利詳情請猛戳: Python、Java、Linux、Go、node、vue、react、javaScript

全棧修煉

相關文章
相關標籤/搜索