這幾在看Vue的源碼與其相關的博客,看到了關於Vue異步更新策略和nextTick的諸多文章,奈何功力不夠深厚,看的是有點矇蔽。主要緣由是這些個模塊,須要對JS的一些運行機制和Event Loop
(事件循環)有必定的瞭解,因而決定再一次深刻的去了解這些知識。前端
在後來幾天的學習中,當下就是總結了這幾個矇蔽點(你可先嚐試自我回答一下):面試
call stack
(執行棧)?task queue
(任務隊列)?task/macrotask
)宏/(microtask
)微事件?當時在學習的過程當中,我就以下圖這樣的感受:ajax
帶着上面這些個問題,因而開始這幾天的展開式的學習,從Event Loop
的概念,在到瀏覽器層面的實際運行原理。瀏覽器
本篇參考諸多文章,借鑑了裏面的不少原話,都在文章的末尾都一一列出了。性能優化
我猜大部分作前端的,都知道Event Loop
(事件循環)的概念。可是不少人,對它的瞭解很是的片面。要想知道這個概念到底是什麼,就要瀏覽器是如何運行的提及。markdown
首先,瀏覽器是多進程執行的,可是對於咱們研究最重要的,就是瀏覽器多個進程中的渲染進程,在瀏覽器的運行中,每個頁面都有獨立渲染進程。這個進程分別由以下幾個線程在工做:數據結構
上面這幾個線程,保證咱們整個頁面(應用)的完整運行。併發
JS引擎線程負責解析Javascript腳本,運行代碼,V8引擎就是在這個線程上運行的。異步
call stack
(執行棧)。全部的任務(函數),最終都會進入這裏來執行。task queue
(任務隊列),一旦任務隊列中有能夠執行的函數,就會壓入棧內執行。如今出現了兩個詞:call stack
(執行棧),task queue
(任務隊列),這裏先來解釋一下,什麼是執行棧。函數
棧是一個先進後出的數據結構,全部的函數都會逐一的進入到這裏面執行,一旦執行完畢就會退出這個棧。
function fun3() { console.log('run'); throw Error('err'); } function fun2() { fun3(); } function fun1() { fun2(); } fun1(); 複製代碼
這裏我特地在fun3
拋出了一個異常,咱們來看一下瀏覽器的輸出:
上面這列出來的一個個函數,就是一個執行棧。這裏我用一個更詳細的圖解來表示一下執行棧的運行過程:
上面這個圖解,是對執行棧運行過程的分佈演示。這個執行棧,就是咱們JS真正運行的地方,函數的調用會在這裏造成一個調用棧,在裏面是一個個執行的,必須得等到棧頂的函數執行完畢退出,才能繼續執行下面的函數。一旦這個棧爲空,它就會去task queue
(任務隊列)看有沒有待執行的任務(函數)。
那麼咱們常說的任務隊列,究竟又是一個啥玩意呢?
首先,這裏還要強調一下上面的提到的,一個頁面的運行,是須要多個線程配合支持的。
我們常說的任務隊列,就是由這個事件觸發線程來維護的。當時,我看到這個就矇蔽了……尼瑪,JS不是單線程嗎?這條事件觸發線程是怎麼回事?
JS的確是仍是單線程執行的。這個事件觸發線程屬於瀏覽器而不是JS引擎,這是用來控制事件循環,而且管理着一個任務隊列(task queue),然而對自己的JS程序,沒有任何控制權限,最終任務隊列裏的函數,仍是得交回執行棧去執行。
那麼這個線程維護的這個task queue
到底是幹嗎的呢?
上面在說call stack
(執行棧)的時候,我們提到了,一旦執行棧裏面被清空了,它就會來看任務隊列中是否有須要執行的任務(函數)。這個任務隊列可能存放着延期執行的回調函數,相似setTimeout
,setInterval
(並非說setTimeout和setInterval在這裏面,而是他們的回調函數),還可能存放着Ajax請求結果的回調函數等等。
這裏先看下具體代碼:
console.log('1'); setTimeout(() => { console.log('2'); }, 1000); $.ajax(/*.....*/) 複製代碼
如今咱們來圖解一下,整個運行過程(圖畫的比較醜,別建議):
因此,如今應該明白call stack
(執行棧),task queue
(任務隊列)是怎麼一個工做狀態了吧。這裏說一句不專業的話,可是你能夠這麼去理解:
在瀏覽器環境下的JS程序運行中,其實並非單線程去完成全部任務的,如定時器的讀數,http的請求,都是交給其餘線程去完成,這樣才能保證JS線程不阻塞。
上面咱們提到在執行setTimeout
和setInterval
的時候,若是讓JS引擎線程去讀數的話,必然會形成阻塞。這也是不符合實際需求的,因此這件讀數的事情,瀏覽器把它交給了渲染進程中的定時器觸發線程。
一旦,代碼中出現timer
類型API,就會交給這個線程去執行,這樣JS引擎線程,就能夠繼續幹別的事情。等到時間一到,這個線程就會將對應的回調,交給事件觸發線程所維護的task queue
(任務隊列)並加入其隊尾,一旦執行棧爲空,就會拿出來執行。
可是這裏要提一點,就算執行棧爲空也不必定能立刻執行這個回調,由於task queue
(任務隊列)中可能還有不少的待執行函數,因此定時器只能讓它到了時間的加入到task queue
中,但不必定可以準時的執行。
這個線程就是專門負責http請求工做的。簡單說就是當執行到一個http異步請求時,就把異步請求事件添加到異步請求線程,等收到響應(準確來講應該是http狀態變化),再把回調函數添加到任務隊列,等待js引擎線程來執行。
這個線程要重點說一下。首先這個GUI渲染線程和JS引擎線程是互斥的,說白了就是這兩個同一時間,只能有一個在運行,JS引擎線程會阻塞GUI渲染線程,這也是爲何JS執行時間長了,會致使頁面渲染不連貫的緣由。
經過上面的這些理論,腦海裏應該大體知道瀏覽器層面的JS是如何去工做了的吧。如今應該能夠回答一開上面提出的部分問題了:
call stack
(執行棧)?答案:執行棧是JS引擎線程(主線程)中的一個先進後出執行JS程序的地方。一次只容許一個函數在執行,一旦棧被清空,將會輪詢任務隊列,將任務隊列中的函數逐一壓如棧內執行task queue
(任務隊列)?答案:任務隊列是在事件觸發線程中,一個存放異步事件回調的地方。當定時器任務,異步請求任務在其餘線程執行完畢時,就會將加入隊列的隊尾,而後被執行棧逐一執行。OK,如今已經解決三個問題,接下來咱們繼續解決剩下的三個問題。這個三個問題,就是從JS事件循環機制的角度來研究了。
我相信大部分搞前端的,都應該知道這玩意。可是,我發現並非每一個人都能說清楚這個東西。完全瞭解這個,對於咱們處理開發中許多異步問題和閱讀源碼,是不少有幫助的。
首先,咱們先開看一張圖(此圖出自於Event Loop的規範和實現):
我以爲若是你看完了上面渲染進程相關知識,在看這個圖,應該是能理解百分之70了吧,剩下百分之是由於裏面出現了microtask queue
(微任務隊列)和Promise
,mutation observer
的相關字眼。
我以爲,在開始瞭解Event Loop
以前,有必要提出兩個問題:
Event Loop
?Event Loop
的每個循環,幹了些什麼事?針對上面給出這個圖出現的一個新詞microtask
,來展開進行學習。
首先,先來看一段代碼:
setTimeout(() => { console.log(1); }); Promise.resolve().then(() => { console.log(2); }); console.log(3); 複製代碼
輸出的結果:3,2,1
在尚未接觸的Event Loop
以前,看到這個結果的時候,說實話,我是很懵逼的。
OK,到這裏,咱們須要先知道兩個概念:task
(任務),microtask
(微任務);
這裏提一點,網上不少博客說到了一個
macrotask
(宏任務)其實跟這個task
(任務)是一個東西。你能夠參考換一下HTML5規範的文檔,裏面甚至沒有macrotask
這個詞,「宏」這個概念,只是爲了更好區分任務和微任務的關係。
經過仔細閱讀文檔得知,這兩個概念屬於對異步任務的分類,不一樣的API註冊的異步任務會依次進入自身對應的隊列中,而後等待Event Loop將它們依次壓入執行棧中執行。
task主要包含:主代碼
、setTimeout
、setInterval
、setImmediate
、I/O
、UI交互事件
microtask主要包含:Promise
、process.nextTick
、MutaionObserver
這裏提一點:Promise的then方法,會將傳入的回調函數,加入到
microtask queue
中。
而後接下來,你須要知道Event Loop
的每一個循環的流程:
microtask
(微任務),直到全部微任務清空爲止如今帶着渲染進程的知識,結合這個流程,來捋一遍上面代碼:
task
,因而它進入JS引擎線程中的執行棧開始執行。setTimeout
方法被調用,也進入執行棧,將一個延時的異步事件交給了定時器觸發線程去讀數,而後它立刻退出執行棧。Promise.resolve()
進入執行棧,返回一個Promise
,退出執行棧。then()
方法進入執行棧,將一個函數加入了microtask queue
(微任務隊列),退出執行棧。console.log(3)
進入執行棧,輸出3,退出執行棧。task
,退出執行棧。microtask queue
,發現一個()=>{ console.log(2) }
函數,壓入執行棧,輸出2,退出執行棧。microtask queue
被清空,切到GUI線程,看是有須要變更UI的,第一輪循環完畢。setTimeout
發起的定時器是在定時器觸發線程併發進行,讀數完畢,回調交給事件觸發線程中的task queue
。因此,此時任務隊列中有一個待執行的task
。task
壓入執行棧執行,輸出1,而後退出執行棧。Event Loop
,繼續重複上面的流程執行。Event Loop
的不斷循環,保證了咱們的JS代碼同步和異步代碼的有序執行。
如今回答一下上面提出的兩個問題:
task
=>microtask
=>GUI
microtask
(微任務)ES6新引入了Promise標準,同時瀏覽器實現上多了一個microtask微任務概念。在瀏覽器上,主要有兩個微任務API:
Promise.then
mutation observer
第一個你們應該都熟悉,第二個呢,我以前也不知道,是後來再看Vue的nextTick源碼中看到的,有興趣的同窗能夠去了解一下這個API。
這裏主要說一下微任務和宏任務的不一樣點,和相同點。
不一樣點:
task
microtask queue
(微任務隊列)。task
執行完畢,而後全部的microtask
都要被執行完。相同點:
如今再來把最開始提出的後三個問題回顧一下,應該有一個大體的概念了吧。
其實原本是想寫,結合Event Loop來理解Vue的異步批量更新以及nextTcik的,可是後面發現Event Loop這塊寫的太多了,因而就分開寫了。。。
可是,我相信你看完上面的所有內容,在面試的時候,或者碰到異步相關問題的時候,都應該可以應付了。其實這個Event Loop
中還有一些用戶交互事件沒詳細講到,有興趣的能夠自行研究一下。
Tips:若是有錯誤或者有歧義的地方,能夠在直接指出。
本篇參考的資料:
「硬核JS」一次搞懂JS運行機制(這篇博客中,說到關於瀏覽器進程和線程的知識,講解的很是詳細,同時對Event Loop也是總結的很是好)
Event Loop的規範和實現(這個主要講Event Loop,也是很是的通俗易懂,裏面的許多案例值得參考)