從開始作前端到目前爲止,陸續看了不少帖子講JS運行機制,看過不久就忘了,仍是本身理一遍好些css
經過碼字使本身對JS運行機制相關內容更加深入(本身用心寫過的貼子,內容也會牢記於心)html
順道給你們看看(我太難了,深夜碼字,反覆修改,說這麼多就是想請你點個贊在看)前端
參考了不少資料(帖子),取其精華,去其糟糠,都在文末,可自行了解html5
是時候搞一波我大js了node
從零到一百再到一,從多方面瞭解JS的運行機制,體會更深入,請認真讀下去git
本文大體分爲如下這樣的步驟來幫助咱們由廣入深更加清晰的瞭解JS運行機制github
JS運行機制在日常前端面試時無論是筆試題仍是面試題命中率都極高web
說到JS運行機制,你知道多少面試
看到這你們可能回說:JS運行機制嘛,很簡單,事件循環、宏微任務那點東西ajax
是的,做爲一名前端咱們都瞭解,可是若是這真的面試問到了這個地方,你真的能夠答好嗎(靈魂一問🤔️)
無論你對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的執行,事件的循環,都在渲染進程內執行,因此咱們要重點了解渲染進程
渲染進程是多線程的,咱們來看渲染進程的一些經常使用較爲主要的線程
<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')
至此主線程執行棧中執行完畢,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:pink'
},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:black'
});
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解釋起來比瀏覽器這繁雜,這裏就只作個對比
關於JS運行機制,我這邊有一個問題,查閱了不少資料,依舊沒有答案
咱們知道,宏任務回調和微任務回調不在同一個隊列,宏任務隊列應該就是事件觸發線程所管理的任務隊列,那麼微任務隊列在哪,由哪一個線程管理,網上的說法不一,因此認真求解,先謝過
上面的流程圖都是本身畫的,因此有點low,見諒
水平有限,歡迎指錯
碼字不易,看完對你有幫助請點贊,有疑問請評論提出
最近拾起了一個被凍結的公衆號,又從新搞了下
歡迎你們關注【不正經的前端】,加我,加羣,或者拿一些資料均可以的,時不時發一些優質原創
做者:isboyjc
郵箱:214930661@qq.com
GitHub: Github.com/isboyjc
參考