從開始作前端到目前爲止,陸續看了不少帖子講 JS 運行機制,看過不久就忘了,仍是本身理一遍好些css
經過碼字使本身對 JS 運行機制相關內容更加深入(本身用心寫過的貼子,內容也會牢記於心)html
順道給你們看看(我太難了,深夜碼字,反覆修改,說這麼多就是想請你點個贊在看)前端
參考了不少資料(帖子),取其精華,去其糟糠,都在文末,可自行了解html5
是時候搞一波我大 js 了node
從零到一百再到一,從多方面瞭解 JS 的運行機制,體會更深入,請認真讀下去web
本文大體分爲如下這樣的步驟來幫助咱們由廣入深更加清晰的瞭解 JS 運行機制面試
原創,首發於掘金ajax
JS 運行機制在日常前端面試時無論是筆試題仍是面試題命中率都極高算法
說到 JS 運行機制,你知道多少segmentfault
看到這你們可能會說:JS 運行機制嘛,很簡單,事件循環、宏微任務那點東西
是的,做爲一名前端咱們都瞭解,可是若是這真的面試問到了這個地方,你真的能夠答好嗎(靈魂一問 ?️)
無論你對 JS 瞭解多少,到這裏你們不防先中止一下閱讀,假設你目前在面試,面試官讓你闡述下 JS 運行機制,思考下你的答案,用 20 秒的時間(面試時 20s 已經很長了),而後帶着答案再接着往下看,有人曾經說過:沒有思考的閱讀純粹是消磨時間罷了
,這話很好(由於是我說的,皮一下 ?)
也有不少剛開始接觸 JS 的同窗會被任務隊列 執行棧 微任務 宏任務
這些高大上點的名字搞的很懵
接下來,咱們來細緻的梳理一遍你就能夠清晰的瞭解它們了
咱們都知道,CPU
是計算機的核心,承擔全部的計算任務
官網說法,進程
是CPU
資源分配的最小單位
字面意思就是進行中的程序,我將它理解爲一個能夠獨立運行且擁有本身的資源空間的任務程序
進程
包括運行中的程序和程序所使用到的內存和系統資源
CPU
能夠有不少進程,咱們的電腦每打開一個軟件就會產生一個或多個進程
,爲何電腦運行的軟件多就會卡,是由於CPU
給每一個進程
分配資源空間,可是一個CPU
一共就那麼多資源,分出去越多,越卡,每一個進程
之間是相互獨立的,CPU
在運行一個進程
時,其餘的進程處於非運行狀態,CPU
使用 時間片輪轉調度算法>) 來實現同時運行多個進程
線程
是CPU
調度的最小單位
線程
是創建在進程
的基礎上的一次程序運行單位,通俗點解釋線程
就是程序中的一個執行流,一個進程
能夠有多個線程
一個進程
中只有一個執行流稱做單線程
,即程序執行時,所走的程序路徑按照連續順序排下來,前面的必須處理好,後面的纔會執行
一個進程
中有多個執行流稱做多線程
,即在一個程序中能夠同時運行多個不一樣的線程
來執行不一樣的任務,
也就是說容許單個程序建立多個並行執行的線程
來完成各自的任務
進程是操做系統分配資源的最小單位,線程是程序執行的最小單位
一個進程由一個或多個線程組成,線程能夠理解爲是一個進程中代碼的不一樣執行路線
進程之間相互獨立,但同一進程下的各個線程間共享程序的內存空間(包括代碼段、數據集、堆等)及一些進程級的資源(如打開文件和信號)
調度和切換:線程上下文切換比進程上下文切換要快得多
多進程:多進程指的是在同一個時間裏,同一個計算機系統中若是容許兩個或兩個以上的進程處於運行狀態。多進程帶來的好處是明顯的,好比你們能夠在網易雲聽歌的同時打開編輯器敲代碼,編輯器和網易雲的進程之間不會相互干擾
多線程:多線程是指程序中包含多個執行流,即在一個程序中能夠同時運行多個不一樣的線程來執行不一樣的任務,也就是說容許單個程序建立多個並行執行的線程來完成各自的任務
JS 的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript 的主要用途是與用戶互動,以及操做 DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定 JavaScript 同時有兩個線程,一個線程在某個 DOM 節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?
還有人說 js 還有 Worker 線程,對的,爲了利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準,容許 JavaScript 腳本建立多個線程,可是子線程是完 全受主線程控制的,並且不得操做 DOM
因此,這個標準並無改變 JavaScript 是單線程的本質
瞭解了進程和線程以後,接下來看看瀏覽器解析,瀏覽器之間也是有些許差距的,不過大體是差很少的,下文咱們皆用市場佔有比例最大的 Chrome 爲例
做爲前端,免不了和瀏覽器打交道,瀏覽器是多進程的,拿 Chrome 來講,咱們每打開一個 Tab 頁就會產生一個進程,咱們使用 Chrome 打開不少標籤頁不關,電腦會愈來愈卡,不說其餘,首先就很耗 CPU
Browser 進程
第三方插件進程
GPU 進程
渲染進程(重)
咱們假設瀏覽器是單進程,那麼某個 Tab 頁崩潰了,就影響了整個瀏覽器,體驗有多差
同理若是插件崩潰了也會影響整個瀏覽器
固然多進程還有其它的諸多優點,不過多闡述
瀏覽器進程有不少,每一個進程又有不少線程,都會佔用內存
這也意味着內存等資源消耗會很大,有點拿空間換時間的意思
到此可不僅是爲了讓咱們理解爲什麼 Chrome 運行時間長了電腦會卡,哈哈,第一個重點來了
頁面的渲染,JS 的執行,事件的循環,都在渲染進程內執行,因此咱們要重點了解渲染進程
渲染進程是多線程的,咱們來看渲染進程的一些經常使用較爲主要的線程
負責渲染瀏覽器界面,解析 HTML,CSS,構建 DOM 樹和 RenderObject 樹,佈局和繪製等
GUI 渲染線程與 JS 引擎線程是互斥的
JS 引擎一直等待着任務隊列中任務的到來,而後加以處理
GUI 渲染線程與 JS 引擎線程是互斥的,js 引擎線程會阻塞 GUI 渲染線程
<script>
標籤,就會中止 GUI 的渲染,而後 js 引擎線程開始工做,執行裏面的 js 代碼,等 js 執行完畢,js 引擎線程中止工做,GUI 繼續渲染下面的內容。因此若是 js 執行時間太長就會形成頁面卡頓的狀況setInterval
與setTimeout
所在線程setTimeout
中低於 4ms 的時間間隔算爲 4ms瞭解了上面這些基礎後,接下來咱們開始進入今天的正題
首先要知道,JS 分爲同步任務和異步任務
同步任務都在主線程(這裏的主線程就是 JS 引擎線程)上執行,會造成一個執行棧
主線程以外,事件觸發線程管理着一個任務隊列
,只要異步任務有了運行結果,就在任務隊列
之中放一個事件回調
一旦執行棧
中的全部同步任務執行完畢(也就是 JS 引擎線程空閒了),系統就會讀取任務隊列
,將可運行的異步任務(任務隊列中的事件回調,只要任務隊列中有事件回調,就說明能夠執行)添加到執行棧中,開始執行
咱們來看一段簡單的代碼
let setTimeoutCallBack = function () { console.log("我是定時器回調") } let httpCallback = function () { console.log("我是http請求回調") } // 同步任務 console.log("我是同步任務1") // 異步定時任務 setTimeout(setTimeoutCallBack, 1000) // 異步http請求任務 ajax.get("/info", httpCallback) // 同步任務 console.log("我是同步任務2")
上述代碼執行過程
JS 是按照順序從上往下依次執行的,能夠先理解爲這段代碼時的執行環境就是主線程,也就是也就是當前執行棧
首先,執行console.log('我是同步任務1')
接着,執行到setTimeout
時,會移交給定時器線程
,通知定時器線程
1s 後將 setTimeoutCallBack
這個回調交給事件觸發線程
處理,在 1s 後事件觸發線程
會收到 setTimeoutCallBack
這個回調並把它加入到事件觸發線程
所管理的事件隊列中等待執行
接着,執行 http 請求,會移交給異步http請求線程
發送網絡請求,請求成功後將 httpCallback
這個回調交由事件觸發線程處理,事件觸發線程
收到 httpCallback
這個回調後把它加入到事件觸發線程
所管理的事件隊列中等待執行
再接着執行console.log('我是同步任務2')
1
至此主線程執行棧中執行完畢,JS引擎線程
已經空閒,開始向事件觸發線程
發起詢問,詢問事件觸發線程
的事件隊列中是否有須要執行的回調函數,若是有將事件隊列中的回調事件加入執行棧中,開始執行回調,若是事件隊列中沒有回調,JS引擎線程
會一直髮起詢問,直到有爲止
到了這裏咱們發現,瀏覽器上的全部線程的工做都很單一且獨立,很是符合單一原則
定時觸發線程只管理定時器且只關注定時不關心結果,定時結束就把回調扔給事件觸發線程
異步 http 請求線程只管理 http 請求一樣不關心結果,請求結束把回調扔給事件觸發線程
事件觸發線程只關心異步回調入事件隊列
而咱們 JS 引擎線程只會執行執行棧中的事件,執行棧中的代碼執行完畢,就會讀取事件隊列中的事件並添加到執行棧中繼續執行,這樣反反覆覆就是咱們所謂的事件循環(Event Loop)
圖解
首先,執行棧開始順序執行
判斷是否爲同步,異步則進入異步線程,最終事件回調給事件觸發線程的任務隊列等待執行,同步繼續執行
執行棧空,詢問任務隊列中是否有事件回調
任務隊列中有事件回調則把回調加入執行棧末尾繼續從第一步開始執行
任務隊列中沒有事件回調則不停發起詢問
在 ECMAScript 中,macrotask
也被稱爲task
咱們能夠將每次執行棧執行的代碼當作是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行), 每個宏任務會從頭至尾執行完畢,不會執行其餘
因爲JS引擎線程
和GUI渲染線程
是互斥的關係,瀏覽器爲了可以使宏任務
和DOM任務
有序的進行,會在一個宏任務
執行結果後,在下一個宏任務
執行前,GUI渲染線程
開始工做,對頁面進行渲染
宏任務 -> GUI渲染 -> 宏任務 -> ...
常見的宏任務
ES6 新引入了 Promise 標準,同時瀏覽器實現上多了一個microtask
微任務概念,在 ECMAScript 中,microtask
也被稱爲jobs
咱們已經知道宏任務
結束後,會執行渲染,而後執行下一個宏任務
, 而微任務能夠理解成在當前宏任務
執行後當即執行的任務
當一個宏任務
執行完,會在渲染前,將執行期間所產生的全部微任務
都執行完
宏任務 -> 微任務 -> GUI渲染 -> 宏任務 -> ...
常見微任務
看了上述宏任務微任務的解釋你可能還不太清楚,不要緊,往下看,先記住那些常見的宏微任務便可
咱們經過幾個例子來看,這幾個例子思路來自掘金雲中君
的文章參考連接【14】,經過渲染背景顏色來區分宏任務和微任務,很直觀,我以爲頗有意思,因此這裏也用這種例子
找一個空白的頁面,在 console 中輸入如下代碼
document.body.style = "background:black" document.body.style = "background:red" document.body.style = "background:blue" document.body.style = "background:pink"
咱們看到上面動圖背景直接渲染了粉紅色,根據上文裏講瀏覽器會先執行完一個宏任務,再執行當前執行棧的全部微任務,而後移交 GUI 渲染,上面四行代碼均屬於同一次宏任務,所有執行完纔會執行渲染,渲染時GUI線程
會將全部 UI 改動優化合並,因此視覺上,只會看到頁面變成粉紅色
再接着看
document.body.style = "background:blue" setTimeout(() => { document.body.style = "background:black" }, 200)
上述代碼中,頁面會先卡一下藍色,再變成黑色背景,頁面上寫的是 200 毫秒,你們能夠把它當成 0 毫秒,由於 0 毫秒的話因爲瀏覽器渲染太快,錄屏很差捕捉,我又沒啥錄屏慢放的工具,你們能夠自行測試的,結果也是同樣,最安全的方法是寫一個index.html
文件,在這個文件中插入上面的 js 腳本,而後瀏覽器打開,谷歌下使用控制檯中performance
功能查看一幀一幀的加載最爲恰當,不過這樣錄屏很差錄因此。。。
迴歸正題,之因此會卡一下藍色,是由於以上代碼屬於兩次宏任務
,第一次宏任務
執行的代碼是將背景變成藍色,而後觸發渲染,將頁面變成藍色,再觸發第二次宏任務將背景變成黑色
再來看
document.body.style = "background:blue" console.log(1) Promise.resolve().then(() => { console.log(2) document.body.style = "background:pink" }) console.log(3)
控制檯輸出 1 3 2 , 是由於 promise 對象的 then 方法的回調函數是異步執行,因此 2 最後輸出
頁面的背景色直接變成粉色,沒有通過藍色的階段,是由於,咱們在宏任務中將背景設置爲藍色,但在進行渲染前執行了微任務, 在微任務中將背景變成了粉色,而後才執行的渲染
瀏覽器會先執行一個宏任務,緊接着執行當前執行棧產生的微任務,再進行渲染,而後再執行下一個宏任務
微任務和宏任務不在一個任務隊列,不在一個任務隊列
setTimeout
是一個宏任務,它的事件回調在宏任務隊列,Promise.then()
是一個微任務,它的事件回調在微任務隊列,兩者並非一個任務隊列此時,你可能還很迷惑,不要緊,請接着往下看
首先執行一個宏任務,執行結束後判斷是否存在微任務
有微任務先執行全部的微任務,再渲染,沒有微任務則直接渲染
而後再接着執行下一個宏任務
首先,總體的 script(做爲第一個宏任務)開始執行的時候,會把全部代碼分爲同步任務
、異步任務
兩部分
同步任務會直接進入主線程依次執行
異步任務會再分爲宏任務和微任務
宏任務進入到 Event Table 中,並在裏面註冊回調函數,每當指定的事件完成時,Event Table 會將這個函數移到 Event Queue 中
微任務也會進入到另外一個 Event Table 中,並在裏面註冊回調函數,每當指定的事件完成時,Event Table 會將這個函數移到 Event Queue 中
當主線程內的任務執行完畢,主線程爲空時,會檢查微任務的 Event Queue,若是有任務,就所有執行,若是沒有就執行下一個宏任務
上述過程會不斷重複,這就是 Event Loop,比較完整的事件循環
new Promise(() => {}).then()
,咱們來看這樣一個 Promise 代碼
前面的 new Promise()
這一部分是一個構造函數,這是一個同步任務
後面的 .then()
纔是一個異步微任務,這一點是很是重要的
new Promise((resolve) => { console.log(1) resolve() }).then(() => { console.log(2) }) console.log(3)
上面代碼輸出1 3 2
async/await 本質上仍是基於 Promise 的一些封裝,而 Promise 是屬於微任務的一種
因此在使用 await 關鍵字與 Promise.then 效果相似
setTimeout(() => console.log(4)) async function test() { console.log(1) await Promise.resolve() console.log(3) } test() console.log(2)
上述代碼輸出1 2 3 4
能夠理解爲,await
之前的代碼,至關於與 new Promise
的同步代碼,await
之後的代碼至關於 Promise.then
的異步
首先給你們來一個比較直觀的動圖
之因此放這個動圖,就是爲了向你們推薦這篇好文,動圖錄屏自參考連接【1】
極力推薦你們看看這篇帖子,很是 nice,分步動畫生動且直觀,有時間的話能夠本身去體驗下
不過在看這個帖子以前你要先了解下運行機制會更好讀懂些
接下來這個來自網上隨意找的一個比較簡單的面試題,求輸出結果
function test() { console.log(1) setTimeout(function () { // timer1 console.log(2) }, 1000) } test() setTimeout(function () { // timer2 console.log(3) }) new Promise(function (resolve) { console.log(4) setTimeout(function () { // timer3 console.log(5) }, 100) resolve() }).then(function () { setTimeout(function () { // timer4 console.log(6) }, 0) console.log(7) }) console.log(8)
結合咱們上述的 JS 運行機制再來看這道題就簡單明瞭的多了
JS 是順序從上而下執行
執行到 test(),test 方法爲同步,直接執行,console.log(1)
打印 1
test 方法中 setTimeout 爲異步宏任務,回調咱們把它記作 timer1 放入宏任務隊列
接着執行,test 方法下面有一個 setTimeout 爲異步宏任務,回調咱們把它記作 timer2 放入宏任務隊列
接着執行 promise,new Promise 是同步任務,直接執行,打印 4
new Promise 裏面的 setTimeout 是異步宏任務,回調咱們記作 timer3 放到宏任務隊列
Promise.then 是微任務,放到微任務隊列
console.log(8)是同步任務,直接執行,打印 8
主線程任務執行完畢,檢查微任務隊列中有 Promise.then
開始執行微任務,發現有 setTimeout 是異步宏任務,記作 timer4 放到宏任務隊列
微任務隊列中的 console.log(7)是同步任務,直接執行,打印 7
微任務執行完畢,第一次循環結束
檢查宏任務隊列,裏面有 timer一、timer二、timer三、timer4,四個定時器宏任務,按照定時器延遲時間獲得能夠執行的順序,即 Event Queue:timer二、timer四、timer三、timer1,依次拿出放入執行棧末尾執行(插播一條:瀏覽器 event loop 的 Macrotask queue,就是宏任務隊列在每次循環中只會讀取一個任務)
執行 timer2,console.log(3)爲同步任務,直接執行,打印 3
檢查沒有微任務,第二次 Event Loop 結束
執行 timer4,console.log(6)爲同步任務,直接執行,打印 6
檢查沒有微任務,第三次 Event Loop 結束
執行 timer3,console.log(5)同步任務,直接執行,打印 5
檢查沒有微任務,第四次 Event Loop 結束
執行 timer1,console.log(2)同步任務,直接執行,打印 2
檢查沒有微任務,也沒有宏任務,第五次 Event Loop 結束
結果:1,4,8,7,3,6,5,2
上面的一切都是針對於瀏覽器的 EventLoop
雖然 NodeJS 中的 JavaScript 運行環境也是 V8,也是單線程,可是,仍是有一些與瀏覽器中的表現是不同的
其實 nodejs 與瀏覽器的區別,就是 nodejs 的宏任務分好幾種類型,而這好幾種又有不一樣的任務隊列,而不一樣的任務隊列又有順序區別,而微任務是穿插在每一種宏任務之間的
在 node 環境下,process.nextTick 的優先級高於 Promise,能夠簡單理解爲在宏任務結束後會先執行微任務隊列中的 nextTickQueue 部分,而後纔會執行微任務中的 Promise 部分
上圖來自 NodeJS 官網
如上圖所示,nodejs 的宏任務分好幾種類型,咱們只簡單介紹大致內容瞭解,不詳細解釋,否則又是囉哩囉嗦一大篇
NodeJS 的 Event Loop 相對比較麻煩
Node會先執行全部類型爲 timers 的 MacroTask,而後執行全部的 MicroTask(NextTick例外) 進入 poll 階段,執行幾乎全部 MacroTask,而後執行全部的 MicroTask 再執行全部類型爲 check 的 MacroTask,而後執行全部的 MicroTask 再執行全部類型爲 close callbacks 的 MacroTask,而後執行全部的 MicroTask 至此,完成一個 Tick,回到 timers 階段 …… 如此反覆,無窮無盡……
反觀瀏覽器中 Event Loop 就比較容易理解
先執行一個 MacroTask,而後執行全部的 MicroTask 再執行一個 MacroTask,而後執行全部的 MicroTask …… 如此反覆,無窮無盡……
好了,關於 Node 中各個類型階段的解析,這裏就不過多說明了,本身查閱資料吧,這裏就是簡單提一下,NodeJS 的 Event Loop 解釋起來比瀏覽器這繁雜,這裏就只作個對比
上面的流程圖都是本身畫的,因此有點 low,見諒
水平有限,歡迎指錯
碼字不易,看完對你有幫助請點贊,有疑問請評論提出
最近拾起了一個被凍結的公衆號,又從新搞了下
歡迎你們關注【不正經的前端】,加我,加羣,或者拿一些資料均可以的,時不時發一些優質原創
參考
- Tasks, microtasks, queues and schedules - 重點推薦閱讀
- 聊聊 JavaScript 與瀏覽器的那些事 - 引擎與線程
- 前端文摘:深刻解析瀏覽器的幕後工做原理
- 瀏覽器進程?線程?傻傻分不清楚!
- 從輸入 cnblogs.com 到博客園首頁徹底展現發生了什麼
- 前端必讀:瀏覽器內部工做原理
- 什麼是 Event Loop?
- JavaScript 運行機制詳解:再談 Event Loop
- 單線程與多線程的區別
- 瀏覽器進程/線程模型及 JS 運行機制
- 瀏覽器的運行機制—2.瀏覽器都包含哪些進程?
- JS 必定要放在 Body 的最底部麼?聊聊瀏覽器的渲染機制
- 從瀏覽器多進程到 JS 單線程,JS 運行機制最全面的一次梳理
- 「前端進階」從多線程到 Event Loop 全面梳理
- Js 基礎知識(四) - js 運行原理與機制
- 這一次,完全弄懂 JavaScript 執行機制
- 前端性能優化:細說瀏覽器渲染的重排與重繪
- 10 分鐘看懂瀏覽器的渲染過程及優化