JS是單執行緒的
JS是單執行緒的,或者說只有一個主執行緒,也就是它一次只能執行一段程式碼。JS中其實是沒有執行緒概念的,所謂的單執行緒也只是相對於多執行緒而言。JS的設計初衷就沒有考慮這些,針對JS這種不具備並行任務處理的特性,我們稱之為“單執行緒”。
雖然JS執行在瀏覽器中是單執行緒的,但是瀏覽器是事件驅動的(Event driven),瀏覽器中很多行為是非同步(Asynchronized)的,會建立事件並放入執行佇列中。瀏覽器中很多非同步行為都是由瀏覽器新開一個執行緒去完成,一個瀏覽器至少實現三個常駐執行緒:
- JS引擎執行緒
- GUI渲染執行緒
- 事件觸發執行緒
JS引擎
JavaScript引擎是一個專門處理JavaScript指令碼的虛擬機器,一般會附帶在網頁瀏覽器之中,比如最出名的就是Chrome瀏覽器的V8引擎,如下圖所示,JS引擎主要有兩個元件構成:
- 堆-記憶體分配發生的地方
- 棧-函式呼叫時會形一個個棧幀(frame)
執行棧
每一個函式執行的時候,都會生成新的execution context(執行上下文),執行上下文會包含一些當前函式的引數、區域性變數之類的資訊,它會被推入棧中, running execution context(正在執行的上下文)始終處於棧的頂部。當函式執行完後,它的執行上下文會從棧彈出。
舉個簡單的例子:function bar() {
console.log('bar');
}
function foo() {
console.log('foo');
bar();
}
foo();
複製程式碼
執行過程中棧的變化:
event loop(事件迴圈)
Wikipedia這樣定義:
"Event Loop是一個程式結構,用於等待和傳送訊息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"
簡單說,就是在程式中設定兩個執行緒:一個負責程式本身的執行,稱為"主執行緒";另一個負責主執行緒與其他程式(主要是各種I/O操作)的通訊,被稱為"Event Loop執行緒"(可以譯為"訊息執行緒")。
事件迴圈與任務佇列
事件迴圈可以簡單描述為:
- 函式入棧,當Stack中執行到非同步任務的時候,就將他丟給WebAPIs,接著執行同步任務,直到Stack為空;
- 在此期間WebAPIs完成這個事件,把回撥函式放入CallbackQueue中等待;
- 當執行棧為空時,Event Loop把Callback Queue中的一個任務放入Stack中,回到第1步。
- Event Loop是由javascript宿主環境(像瀏覽器)來實現的;
- WebAPIs是由C++實現的瀏覽器建立的執行緒,處理諸如DOM事件、http請求、定時器等非同步事件;
- JavaScript 的併發模型基於"事件迴圈";
- Callback Queue(Event Queue 或者 Message Queue) 任務佇列,存放非同步任務的回撥函式
接下來看一個非同步函式執行的例子:
var start=new Date();
setTimeout(function cb(){
console.log("時間間隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
複製程式碼
- main(Script) 函式入棧,start變數開始初始化
- setTimeout入棧,出棧,丟給WebAPIs,開始定時500ms;
- while迴圈入棧,開始阻塞1000ms;
- 500ms過後,WebAPIs把cb()放入任務佇列,此時while迴圈還在棧中,cb()等待;
- 又過了500ms,while迴圈執行完畢從棧中彈出,main()彈出,此時棧為空,Event Loop,cb()進入棧,log()進棧,輸出'時間間隔:1003ms',出棧,cb()出棧
巨集任務(Macrotasks)和微任務(Microtasks)
其實我們上面所說的都是巨集任務(Macrotasks),但是js中還有一種佇列微任務(Microtasks)。
macro-task(Task):一個event loop有一個或者多個task佇列。task任務源非常寬泛,比如ajax的onload,click事件,基本上我們經常繫結的各種事件都是task任務源,還有資料庫操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任務源。總結來說task任務源:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
micro-task(Job):microtask 佇列和task 佇列有些相似,都是先進先出的佇列,由指定的任務源去提供任務,不同的是一個 event loop裡只有一個microtask 佇列。另外microtask執行時機和Macrotasks也有所差異
- process.nextTick
- promises
- Object.observe
- MutationObserver
那麼Macrotasks和Microtasks有什麼別區別呢
舉個簡單的例子,假設一個script標籤的程式碼如下:
Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
Promise.resolve().then(function promise2 () {
console.log('promise2');
})
}, 0)
setTimeout(function setTimeout2 (){
console.log('setTimeout2')
}, 0)
複製程式碼
執行過程:
script裡的程式碼被列為一個task,放入task佇列。
迴圈1:
- 【task佇列:script ;microtask佇列:】
- 從task佇列中取出script任務,推入棧中執行。
- promise1列為microtask,setTimeout1列為task,setTimeout2列為task。
- 【task佇列:setTimeout1 setTimeout2;microtask佇列:promise1】
- script任務執行完畢,執行microtask checkpoint,取出microtask佇列的promise1執行。
迴圈2:
*【task佇列:setTimeout1 setTimeout2;microtask佇列:】 4. 從task佇列中取出setTimeout1,推入棧中執行,將promise2列為microtask。
- 【task佇列:setTimeout2;microtask佇列:promise2】
- 執行microtask checkpoint,取出microtask佇列的promise2執行。
迴圈3:
- 【task佇列:setTimeout2;microtask佇列:】
- 從task佇列中取出setTimeout2,推入棧中執行。 7.setTimeout2任務執行完畢,執行microtask checkpoint。
- 【task佇列:;microtask佇列:】
綜上所說,每次event loop迴圈執行棧完成後,會繼續執行完相應的microtask任務
event loop中的Update the rendering(更新渲染)
這是event loop中很重要部分,在第7步會進行Update the rendering(更新渲染),規範允許瀏覽器自己選擇是否更新檢視。也就是說可能不是每輪事件迴圈都去更新檢視,只在有必要的時候才更新檢視。