你知道的越多,你不知道的越多
點贊
再看,手留餘香,與有榮焉javascript
幾乎在每一本JS相關的書籍中,都會說JS是單線程
的,JS是經過事件隊列(Event Loop)
的方式來實現異步回調的。 對不少初學JS的人來講,根本搞不清楚單線程的JS爲何擁有異步
的能力,因此,我試圖從進程
、線程
的角度來解釋這個問題。html
計算機的核心是CPU
,它承擔了全部的計算任務。前端
它就像一座工廠,時刻在運行。java
假定工廠的電力有限,一次只能供給一個車間使用。 也就是說,一個車間開工的時候,其餘車間都必須停工。 背後的含義就是,單個CPU一次只能運行一個任務。ajax
進程
就比如工廠的車間,它表明CPU所能處理的單個任務。 進程
之間相互獨立,任一時刻,CPU老是運行一個進程
,其餘進程
處於非運行狀態。 CPU使用時間片輪轉進度算法來實現同時運行多個進程
。算法
一個車間裏,能夠有不少工人,共享車間全部的資源,他們協同完成一個任務。windows
線程
就比如車間裏的工人,一個進程
能夠包括多個線程
,多個線程
共享進程
資源。數組
從上文咱們已經簡單瞭解了CPU、進程、線程,簡單彙總一下。promise
進程
是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)線程
是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)進程
之間也能夠通訊,不過代價較大單線程
與多線程
,都是指在一個進程
內的單和多咱們已經知道了CPU
、進程
、線程
之間的關係,對於計算機來講,每個應用程序都是一個進程
, 而每個應用程序都會分別有不少的功能模塊,這些功能模塊其實是經過子進程
來實現的。 對於這種子進程
的擴展方式,咱們能夠稱這個應用程序是多進程
的。瀏覽器
而對於瀏覽器來講,瀏覽器就是多進程的,我在Chrome瀏覽器中打開了多個tab,而後打開windows控制管理器:
如上圖,咱們能夠看到一個Chrome瀏覽器啓動了好多個進程。
總結一下:
渲染進程
,就是咱們說的瀏覽器內核
那麼瀏覽器中包含了這麼多的進程,那麼對於普通的前端操做來講,最重要的是什麼呢?
答案是渲染進程
,也就是咱們常說的瀏覽器內核
從前文咱們得知,進程和線程是一對多的關係,也就是說一個進程包含了多條線程。
而對於渲染進程
來講,它固然也是多線程的了,接下來咱們來看一下渲染進程包含哪些線程。
GUI渲染線程
JS引擎線程
事件觸發線程
定時觸發器線程
異步http請求線程
當咱們瞭解了渲染進程包含的這些線程後,咱們思考兩個問題:
首先是歷史緣由,在建立 javascript 這門語言時,多進程多線程的架構並不流行,硬件支持並很差。
其次是由於多線程的複雜性,多線程操做須要加鎖,編碼的複雜性會增高。
並且,若是同時操做 DOM ,在多線程不加鎖的狀況下,最終會致使 DOM 渲染的結果不可預期。
這是因爲 JS 是能夠操做 DOM 的,若是同時修改元素屬性並同時渲染界面(即 JS線程
和UI線程
同時運行), 那麼渲染線程先後得到的元素就可能不一致了。
所以,爲了防止渲染出現不可預期的結果,瀏覽器設定 GUI渲染線程
和JS引擎線程
爲互斥關係, 當JS引擎線程
執行時GUI渲染線程
會被掛起,GUI更新則會被保存在一個隊列中等待JS引擎線程
空閒時當即被執行。
到了這裏,終於要進入咱們的主題,什麼是 Event Loop
先理解一些概念:
執行棧
任務隊列
,異步任務觸發條件達成,將回調事件放到任務隊列
中執行棧
中全部同步任務執行完畢,此時JS引擎線程空閒,系統會讀取任務隊列
,將可運行的異步任務回調事件添加到執行棧
中,開始執行在前端開發中咱們會經過setTimeout/setInterval
來指定定時任務,會經過XHR/fetch
發送網絡請求, 接下來簡述一下setTimeout/setInterval
和XHR/fetch
到底作了什麼事
咱們知道,不論是setTimeout/setInterval
和XHR/fetch
代碼,在這些代碼執行時, 自己是同步任務,而其中的回調函數纔是異步任務。
當代碼執行到setTimeout/setInterval
時,其實是JS引擎線程
通知定時觸發器線程
,間隔一個時間後,會觸發一個回調事件, 而定時觸發器線程
在接收到這個消息後,會在等待的時間後,將回調事件放入到由事件觸發線程
所管理的事件隊列
中。
當代碼執行到XHR/fetch
時,其實是JS引擎線程
通知異步http請求線程
,發送一個網絡請求,並制定請求完成後的回調事件, 而異步http請求線程
在接收到這個消息後,會在請求成功後,將回調事件放入到由事件觸發線程
所管理的事件隊列
中。
當咱們的同步任務執行完,JS引擎線程
會詢問事件觸發線程
,在事件隊列
中是否有待執行的回調函數,若是有就會加入到執行棧中交給JS引擎線程
執行
用一張圖來解釋:
再用代碼來解釋一下:
let timerCallback = function() {
console.log('wait one second');
};
let httpCallback = function() {
console.log('get server data success');
}
// 同步任務
console.log('hello');
// 同步任務
// 通知定時器線程 1s 後將 timerCallback 交由事件觸發線程處理
// 1s 後事件觸發線程將 timerCallback 加入到事件隊列中
setTimeout(timerCallback,1000);
// 同步任務
// 通知異步http請求線程發送網絡請求,請求成功後將 httpCallback 交由事件觸發線程處理
// 請求成功後事件觸發線程將 httpCallback 加入到事件隊列中
$.get('www.xxxx.com',httpCallback);
// 同步任務
console.log('world');
//...
// 全部同步任務執行完後
// 詢問事件觸發線程在事件事件隊列中是否有須要執行的回調函數
// 若是沒有,一直詢問,直到有爲止
// 若是有,將回調事件加入執行棧中,開始執行回調代碼
複製代碼
總結一下:
當咱們基本瞭解了什麼是執行棧,什麼是事件隊列以後,咱們深刻了解一下事件循環中宏任務
、微任務
咱們能夠將每次執行棧執行的代碼當作是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行), 每個宏任務會從頭至尾執行完畢,不會執行其餘。
咱們前文提到過JS引擎線程
和GUI渲染線程
是互斥的關係,瀏覽器爲了可以使宏任務
和DOM任務
有序的進行,會在一個宏任務
執行結果後,在下一個宏任務
執行前,GUI渲染線程
開始工做,對頁面進行渲染。
// 宏任務-->渲染-->宏任務-->渲染-->渲染...
複製代碼
主代碼塊,setTimeout,setInterval等,都屬於宏任務
第一個例子:
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';
複製代碼
咱們能夠將這段代碼放到瀏覽器的控制檯執行如下,看一下效果:
咱們會看到的結果是,頁面背景會在瞬間變成灰色,以上代碼屬於同一次宏任務
,因此所有執行完才觸發頁面渲染
,渲染時GUI線程
會將全部UI改動優化合並,因此視覺效果上,只會看到頁面變成灰色。
第二個例子:
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)
複製代碼
執行一下,再看效果:
我會看到,頁面先顯示成藍色背景,而後瞬間變成了黑色背景,這是由於以上代碼屬於兩次宏任務
,第一次宏任務
執行的代碼是將背景變成藍色,而後觸發渲染,將頁面變成藍色,再觸發第二次宏任務將背景變成黑色。
咱們已經知道宏任務
結束後,會執行渲染,而後執行下一個宏任務
, 而微任務能夠理解成在當前宏任務
執行後當即執行的任務。
也就是說,當宏任務
執行完,會在渲染前,將執行期間所產生的全部微任務
都執行完。
Promise,process.nextTick等,屬於微任務
。
第一個例子:
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(() => {
console.log(1)
Promise.resolve(3).then(data => console.log(data))
}, 0)
setTimeout(() => {
console.log(2)
}, 0)
// print : 1 3 2
複製代碼
上面代碼共包含兩個 setTimeout ,也就是說除主代碼塊外,共有兩個宏任務
, 其中第一個宏任務
執行中,輸出 1 ,而且建立了微任務隊列
,因此在下一個宏任務
隊列執行前, 先執行微任務
,在微任務
執行中,輸出 3 ,微任務執行後,執行下一次宏任務
,執行中輸出 2
宏任務
(棧中沒有就從事件隊列
中獲取)微任務
,就將它添加到微任務
的任務隊列中宏任務
執行完畢後,當即執行當前微任務隊列
中的全部微任務
(依次執行)宏任務
執行完畢,開始檢查渲染,而後GUI線程
接管渲染JS線程
繼續接管,開始下一個宏任務
(從事件隊列中獲取)