JavaScript 運行機制

1. 單線程的JavaScript
JavaScript是單線程的語言這,由它的用途決定的,作為瀏覽器的腳本語言,主要負責和用戶交互,操作DOM。

假如JavaScript是多線程的,有兩個線程同時操作一個DOM節點,一個負責刪除DOM節點,一個在DOM節點上添加內容,瀏覽器該以哪個線程為標準呢?

所以,JavaScript的用途決定它只能是單線程的,過去是,將來也不會變。

HTML5的Web Worker允許JavaScript主線程創建多個子線程,但是這些子線程完全受主線程的控制,且不可操作DOM節點,所以JavaScript單線程的本質并沒有發生改變。

2. 同步任務和異步任務
JavaScript是單線程語言,就意味著任務需要排隊執行,只有前一個執行完成,后一個才可以執行。

如果前一個任務非常耗時呢?比如操作IO設備、網絡請求等,后面的任務就會被阻塞,頁面就會被卡住,甚至崩潰,用戶體驗非常差。

如果JavaScript的主線程在遇到這些耗時的任務時,將其掛起,先執行后面的任務,等掛起的任務有結果以后再回頭執行,這樣就可以解決耗時任務阻塞主線程的問題了。

于是,所有的任務就可以分為兩種,同步任務和異步任務,同步任務放在主線程中執行,異步任務被掛起,不進入主線程執行(讓主線程阻塞等待),當其有結果了,再放入主線程中執行。

3. 任務隊列和Event Loop

3.1 任務隊列

任務隊列是一個事件隊列,也可以理解成消息隊列,當掛起的異步任務就緒以后就會在任務隊列中放置相應的事件,表示該任務可以進入主線程中執行了。

任務隊列中的事件,除了IO設備的事件,還有網絡請求,鼠標點擊、滾動等,只要為事件指定過回調函數,這些事件發生時就會進入任務隊列,等待主線程來讀取,然后執行相應的回調函數。

回調函數其實就是被掛起來的異步任務,比如:Ajax請求,請求成功或失敗以后執行的回調函數就是異步任務。

任務隊列是一個先進先出的數據結構,排在前面的事件,只要主線程一空,就會優先被讀取。

3.2 Event Loop

主線程從任務隊列讀取事件,這個過程是循環不斷的,所以JavaScript這種運行機制又稱為Event Loop(事件循環)
4. 宏任務和微任務
異步任務可進一步劃分為宏任務和微任務,相應的任務隊列也有兩種,分別為宏任務隊列和微任務隊列。

4.1 宏任務

setTimeout、setInterval、setImmediate會產生宏任務

4.2 微任務

requestAnimationFrame、IO、讀取數據、交互事件、UI render、Promise.then、MutationObserve、process.nextTick會產生微任務

4.3 瀏覽器中的JavaScript腳本執行過程

4.3.1 過程描述

a. JavaScript腳本進入主線程, 開始執行

b. 執行過程中如果遇到宏任務和微任務,分別將其掛起,只有當任務就緒時將事件放入相應的任務隊列

c. 腳本執行完成,執行棧清空

d. 去微任務隊列依次讀取事件,并將相應的回調函數放入執行棧運行,如果執行過程中遇到宏任務和微任務,處理方式同 b, 直到微任務隊列為空

e. 瀏覽器執行渲染動作, GUI渲染線程接管,直到渲染結束

f. JS線程接管,去宏任務隊列依次讀取事件,并將相應的回調函數放入執行棧, 開始下一個宏任務的執行,過程為b -> c -> d -> e -> f, 如此循環

g. 直到執行棧、宏任務隊列、微任務隊列都為空,腳本執行結束

4.3.2 示例

// 腳本
console.log(1)
setTimeout(() => {
  console.log(2)
}, 0)
const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log(3)
    resolve()
  }, 1000)
  console.log(4)
})
p.then(() => {
  console.log(5)
})
console.log(6)

執行過程

a. 腳本放入執行棧開始實行

b. 執行到console.log(1), 輸入1

c. 執行到setTimeout,遇到宏任務,將其掛起,由于延時 0ms,將在 4ms后在宏任務隊列產生一個定時事件, 我們叫定時A

d. 程序繼續向下執行,執行new Promise(),并運行其參數,遇到第二個定時任務(宏任務),叫它定時B,并將其掛起,執行console.log(4), 輸出4

e. 遇到微任務p.then(), 將其掛起

f. 向下執行遇到console.log(6), 輸出6

g. 執行棧清空,讀取微任務隊列,發現為空,因為p.then()含沒有就緒,它的就緒依賴與第一個定時任務(定時A)的執行

h. 執行棧為空,微任務隊列為空,執行瀏覽器的渲染動作

i. 讀取宏任務隊列,讀取第一個就緒的宏任務,為定時任務A,將其回調函數放入執行棧開始執行,執行console.log(2), 輸入2

j. 執行棧清空,微任務隊列為空,渲染

k. 開始執行下一個就緒的宏任務,定時任務B,并將其回調函數放入執行棧執行,執行console.log(3), 輸出3,并執行resolve(), p.then()就緒,在微任務隊列放入相應的事件

o. 執行棧清空,讀取微任務隊列,發現不為空,讀取第一個就緒的事件,并將其對應的回調函數放入執行棧執行,執行console.log(5), 輸出5

p. 執行棧清空,微任務隊列為空,渲染,然后發現宏任務隊列為空,本次腳本執行徹底結束

輸出結果為: 1 4 6 2 3 5

4.3.3 外鏈

4.3.4 總結

如果把JavaScript腳本也當作初始的宏任務,那么JavaScript在瀏覽器端的執行過程就是這樣:

先執行一個宏任務, 然后執行所有的微任務

再執行一個宏任務,然后執行所有的微任務

...

如此反復,執行執行棧和任務隊列為空

4.4 node.js中JavaScript腳本的執行過程

JavaScript腳本執行過程在node.js和瀏覽器中有些不同, 造成這些差異的原因在于,瀏覽器中只有一個宏任務隊列,但是node.js中有好幾個宏任務隊列,而且這些宏任務隊列還有執行的先后順序,而微任務時穿插在這些宏任務之間執行的

4.4.1 執行順序

// 各個事件類型, 實行順序自上而下
   ┌───────────────────────┐
┌─>│        timers         │
<h3>4.4.2 示例</h3>
<h4>4.4.2.1 基本示例</h4>

console.log(1)

setTimeout(() => { console.log('timer1') Promise.resolve().then(() => { console.log('promise1') }) }, 0)

setTimeout(() => { console.log('timer2') Promise.resolve().then(() => { console.log('promise2') }) }, 0)

console.log(2)

<blockquote>這段代碼在瀏覽器中的執行結果為:1 2 timer1 promise1 timer2 promise2<p>在node.js中的執行結果則為:1 2 timer1 timer2 promise1 promise2</p>
</blockquote>
<h3>4.4.2.2 setTimeout和setImmediate的順序</h3>
<blockquote>它們兩個順序從上圖看顯而易見,timers隊列在check隊列執行運行,但是有個前提,事件已經就緒</blockquote>

setTimeout(() => { console.log('timeout') }, 0)

setImmediate(() => { console.log('immediate') })

<blockquote>以上代碼在node.js中的運行結果為:immediate timeout,原因如下:<p>在程序運行時timer事件未就緒,所以第一次去讀timer隊列時,隊列為空,繼續向下執行,在check隊列讀取到了就緒的事件注册送28体验金的游戏平台,所以先執行immediate,再執行timeout,因為即使setTimeout的延時時間未 0,但是node.js一般會設置為 1ms, 所以,當node準備Event Loop的時間大于 1ms時,就會先輸出timeout,后輸出immediate,否則先輸出immediate后輸出timeout</p>
</blockquote>

const fs = require('fs')

// 讀取文件 fs.readFile('xx.txt', () => { setTimeout(() => { console.log('timeout') })

setImmediate(() => { console.log('immediate') }) })

```

以上代碼的輸出順序一定為:immediate timeout, 原因如下:

setTimeout和setImmediate都寫在I/O callback中,意味著處于poll階段,然后是check階段,所以,此時無論setTimeout就緒多快(1ms),都會優先執行setImmediate,本質上注册送28体验金的游戏平台,從poll階段開始執行,而不是一個Tick初始階段。

原文鏈接:

上一篇:h5 draggable 拖動元素跟實際結構表現不匹配的問題
下一篇:編寫可復用的組件,我們可以收獲很多

相關推薦

官方社區

掃碼加入 JavaScript 社區