溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

「前端進階」從多線程到Event Loop全面梳理

發(fā)布時間:2020-05-16 01:01:34 來源:網(wǎng)絡(luò) 閱讀:496 作者:可樂程序員 欄目:web開發(fā)

引子

幾乎在每一本JS相關(guān)的書籍中,都會說JS是單線程的,JS是通過事件隊列(Event Loop)的方式來實現(xiàn)異步回調(diào)的。 對很多初學JS的人來說,根本搞不清楚單線程的JS為什么擁有異步的能力,所以,我試圖從進程、線程的角度來解釋這個問題。

CPU

「前端進階」從多線程到Event Loop全面梳理


計算機的核心是CPU,它承擔了所有的計算任務(wù)。

它就像一座工廠,時刻在運行。

進程

「前端進階」從多線程到Event Loop全面梳理


假定工廠的電力有限,一次只能供給一個車間使用。 也就是說,一個車間開工的時候,其他車間都必須停工。 背后的含義就是,單個CPU一次只能運行一個任務(wù)。

進程就好比工廠的車間,它代表CPU所能處理的單個任務(wù)。 進程之間相互獨立,任一時刻,CPU總是運行一個進程,其他進程處于非運行狀態(tài)。 CPU使用時間片輪轉(zhuǎn)進度算法來實現(xiàn)同時運行多個進程。

線程

「前端進階」從多線程到Event Loop全面梳理


一個車間里,可以有很多工人,共享車間所有的資源,他們協(xié)同完成一個任務(wù)。

線程就好比車間里的工人,一個進程可以包括多個線程,多個線程共享進程資源。

CPU、進程、線程之間的關(guān)系

從上文我們已經(jīng)簡單了解了CPU、進程、線程,簡單匯總一下。

  • 進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)

  • 線程是cpu調(diào)度的最小單位(線程是建立在進程的基礎(chǔ)上的一次程序運行單位,一個進程中可以有多個線程)

  • 不同進程之間也可以通信,不過代價較大

  • 單線程與多線程,都是指在一個進程內(nèi)的單和多

瀏覽器是多進程的

我們已經(jīng)知道了CPU、進程、線程之間的關(guān)系,對于計算機來說,每一個應(yīng)用程序都是一個進程, 而每一個應(yīng)用程序都會分別有很多的功能模塊,這些功能模塊實際上是通過子進程來實現(xiàn)的。 對于這種子進程的擴展方式,我們可以稱這個應(yīng)用程序是多進程的。

而對于瀏覽器來說,瀏覽器就是多進程的,我在Chrome瀏覽器中打開了多個tab,然后打開windows控制管理器:

「前端進階」從多線程到Event Loop全面梳理


如上圖,我們可以看到一個Chrome瀏覽器啟動了好多個進程。

總結(jié)一下:

  • 瀏覽器是多進程的

  • 每一個Tab頁,就是一個獨立的進程

瀏覽器包含了哪些進程

  • 主進程

  • 協(xié)調(diào)控制其他子進程(創(chuàng)建、銷毀)

  • 瀏覽器界面顯示,用戶交互,前進、后退、收藏

  • 將渲染進程得到的內(nèi)存中的Bitmap,繪制到用戶界面上

  • 處理不可見操作,網(wǎng)絡(luò)請求,文件訪問等

  • 第三方插件進程

  • 每種類型的插件對應(yīng)一個進程,僅當使用該插件時才創(chuàng)建

  • GPU進程

  • 用于3D繪制等

  • 渲染進程,就是我們說的瀏覽器內(nèi)核負責頁面渲染,腳本執(zhí)行,事件處理等

  • 每個tab頁一個渲染進程

那么瀏覽器中包含了這么多的進程,那么對于普通的前端操作來說,最重要的是什么呢?

答案是渲染進程,也就是我們常說的瀏覽器內(nèi)核

瀏覽器內(nèi)核(渲染進程)

從前文我們得知,進程和線程是一對多的關(guān)系,也就是說一個進程包含了多條線程。

而對于渲染進程來說,它當然也是多線程的了,接下來我們來看一下渲染進程包含哪些線程。

  • GUI渲染線程負責渲染頁面,布局和繪制

  • 頁面需要重繪和回流時,該線程就會執(zhí)行

  • 與js引擎線程互斥,防止渲染結(jié)果不可預(yù)期

  • JS引擎線程負責處理解析和執(zhí)行javascript腳本程序

  • 只有一個JS引擎線程(單線程)

  • 與GUI渲染線程互斥,防止渲染結(jié)果不可預(yù)期

  • 事件觸發(fā)線程用來控制事件循環(huán)(鼠標點擊、setTimeout、ajax等)

  • 當事件滿足觸發(fā)條件時,將事件放入到JS引擎所在的執(zhí)行隊列中

  • 定時觸發(fā)器線程setInterval與setTimeout所在的線程

  • 定時任務(wù)并不是由JS引擎計時的,是由定時觸發(fā)線程來計時的

  • 計時完畢后,通知事件觸發(fā)線程

  • 異步http請求線程瀏覽器有一個單獨的線程用于處理AJAX請求

  • 當請求完成時,若有回調(diào)函數(shù),通知事件觸發(fā)線程

當我們了解了渲染進程包含的這些線程后,我們思考兩個問題:

  1. 為什么 javascript 是單線程的

  2. 為什么 GUI 渲染線程為什么與 JS 引擎線程互斥

為什么 javascript 是單線程的

首先是歷史原因,在創(chuàng)建 javascript 這門語言時,多進程多線程的架構(gòu)并不流行,硬件支持并不好。

其次是因為多線程的復雜性,多線程操作需要加鎖,編碼的復雜性會增高。

而且,如果同時操作 DOM ,在多線程不加鎖的情況下,最終會導致 DOM 渲染的結(jié)果不可預(yù)期。

為什么 GUI 渲染線程與 JS 引擎線程互斥

這是由于 JS 是可以操作 DOM 的,如果同時修改元素屬性并同時渲染界面(即 JS線程和UI線程同時運行), 那么渲染線程前后獲得的元素就可能不一致了。

因此,為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)定 GUI渲染線程和JS引擎線程為互斥關(guān)系, 當JS引擎線程執(zhí)行時GUI渲染線程會被掛起,GUI更新則會被保存在一個隊列中等待JS引擎線程空閑時立即被執(zhí)行。

從 Event Loop 看 JS 的運行機制

到了這里,終于要進入我們的主題,什么是 Event Loop

先理解一些概念:

  • JS 分為同步任務(wù)和異步任務(wù)

  • 同步任務(wù)都在JS引擎線程上執(zhí)行,形成一個執(zhí)行棧

  • 事件觸發(fā)線程管理一個任務(wù)隊列,異步任務(wù)觸發(fā)條件達成,將回調(diào)事件放到任務(wù)隊列中

  • 執(zhí)行棧中所有同步任務(wù)執(zhí)行完畢,此時JS引擎線程空閑,系統(tǒng)會讀取任務(wù)隊列,將可運行的異步任務(wù)回調(diào)事件添加到執(zhí)行棧中,開始執(zhí)行

「前端進階」從多線程到Event Loop全面梳理


在前端開發(fā)中我們會通過setTimeout/setInterval來指定定時任務(wù),會通過XHR/fetch發(fā)送網(wǎng)絡(luò)請求, 接下來簡述一下setTimeout/setInterval和XHR/fetch到底做了什么事

我們知道,不管是setTimeout/setInterval和XHR/fetch代碼,在這些代碼執(zhí)行時, 本身是同步任務(wù),而其中的回調(diào)函數(shù)才是異步任務(wù)。

當代碼執(zhí)行到setTimeout/setInterval時,實際上是JS引擎線程通知定時觸發(fā)器線程,間隔一個時間后,會觸發(fā)一個回調(diào)事件, 而定時觸發(fā)器線程在接收到這個消息后,會在等待的時間后,將回調(diào)事件放入到由事件觸發(fā)線程所管理的事件隊列中。

當代碼執(zhí)行到XHR/fetch時,實際上是JS引擎線程通知異步http請求線程,發(fā)送一個網(wǎng)絡(luò)請求,并制定請求完成后的回調(diào)事件, 而異步http請求線程在接收到這個消息后,會在請求成功后,將回調(diào)事件放入到由事件觸發(fā)線程所管理的事件隊列中。

當我們的同步任務(wù)執(zhí)行完,JS引擎線程會詢問事件觸發(fā)線程,在事件隊列中是否有待執(zhí)行的回調(diào)函數(shù),如果有就會加入到執(zhí)行棧中交給JS引擎線程執(zhí)行

用一張圖來解釋:

「前端進階」從多線程到Event Loop全面梳理


再用代碼來解釋一下:

let?timerCallback?=?function()?{
?console.log('wait?one?second');
};
let?httpCallback?=?function()?{
?console.log('get?server?data?success');
}
//?同步任務(wù)
console.log('hello');
//?同步任務(wù)
//?通知定時器線程?1s?后將?timerCallback?交由事件觸發(fā)線程處理
//?1s?后事件觸發(fā)線程將?timerCallback?加入到事件隊列中
setTimeout(timerCallback,1000);
//?同步任務(wù)
//?通知異步http請求線程發(fā)送網(wǎng)絡(luò)請求,請求成功后將?httpCallback?交由事件觸發(fā)線程處理
//?請求成功后事件觸發(fā)線程將?httpCallback?加入到事件隊列中
$.get('www.xxxx.com',httpCallback);
//?同步任務(wù)
console.log('world');
//...
//?所有同步任務(wù)執(zhí)行完后
//?詢問事件觸發(fā)線程在事件事件隊列中是否有需要執(zhí)行的回調(diào)函數(shù)
//?如果沒有,一直詢問,直到有為止
//?如果有,將回調(diào)事件加入執(zhí)行棧中,開始執(zhí)行回調(diào)代碼
復制代碼

總結(jié)一下:

  • JS引擎線程只執(zhí)行執(zhí)行棧中的事件

  • 執(zhí)行棧中的代碼執(zhí)行完畢,就會讀取事件隊列中的事件

  • 事件隊列中的回調(diào)事件,是由各自線程插入到事件隊列中的

  • 如此循環(huán)

宏任務(wù)、微任務(wù)

當我們基本了解了什么是執(zhí)行棧,什么是事件隊列之后,我們深入了解一下事件循環(huán)中宏任務(wù)、微任務(wù)

什么是宏任務(wù)

我們可以將每次執(zhí)行棧執(zhí)行的代碼當做是一個宏任務(wù)(包括每次從事件隊列中獲取一個事件回調(diào)并放到執(zhí)行棧中執(zhí)行), 每一個宏任務(wù)會從頭到尾執(zhí)行完畢,不會執(zhí)行其他。

我們前文提到過JS引擎線程和GUI渲染線程是互斥的關(guān)系,瀏覽器為了能夠使宏任務(wù)和DOM任務(wù)有序的進行,會在一個宏任務(wù)執(zhí)行結(jié)果后,在下一個宏任務(wù)執(zhí)行前,GUI渲染線程開始工作,對頁面進行渲染。

//?宏任務(wù)-->渲染-->宏任務(wù)-->渲染-->渲染...
復制代碼

主代碼塊,setTimeout,setInterval等,都屬于宏任務(wù)

第一個例子:

document.body.style?=?'background:black';
document.body.style?=?'background:red';
document.body.style?=?'background:blue';
document.body.style?=?'background:grey';
復制代碼

我們可以將這段代碼放到瀏覽器的控制臺執(zhí)行以下,看一下效果:

「前端進階」從多線程到Event Loop全面梳理


我們會看到的結(jié)果是,頁面背景會在瞬間變成灰色,以上代碼屬于同一次宏任務(wù),所以全部執(zhí)行完才觸發(fā)頁面渲染,渲染時GUI線程會將所有UI改動優(yōu)化合并,所以視覺效果上,只會看到頁面變成灰色。

第二個例子:

document.body.style?=?'background:blue';
setTimeout(function(){
?document.body.style?=?'background:black'
},0)
復制代碼

執(zhí)行一下,再看效果:

「前端進階」從多線程到Event Loop全面梳理


我會看到,頁面先顯示成藍色背景,然后瞬間變成了黑色背景,這是因為以上代碼屬于兩次宏任務(wù),第一次宏任務(wù)執(zhí)行的代碼是將背景變成藍色,然后觸發(fā)渲染,將頁面變成藍色,再觸發(fā)第二次宏任務(wù)將背景變成黑色。

什么是微任務(wù)

我們已經(jīng)知道宏任務(wù)結(jié)束后,會執(zhí)行渲染,然后執(zhí)行下一個宏任務(wù), 而微任務(wù)可以理解成在當前宏任務(wù)執(zhí)行后立即執(zhí)行的任務(wù)。

也就是說,當宏任務(wù)執(zhí)行完,會在渲染前,將執(zhí)行期間所產(chǎn)生的所有微任務(wù)都執(zhí)行完。

Promise,process.nextTick等,屬于微任務(wù)。

第一個例子:

document.body.style?=?'background:blue'
console.log(1);
Promise.resolve().then(()=>{
?console.log(2);
?document.body.style?=?'background:black'
});
console.log(3);
復制代碼

執(zhí)行一下,再看效果:

「前端進階」從多線程到Event Loop全面梳理


控制臺輸出 1 3 2 , 是因為 promise 對象的 then 方法的回調(diào)函數(shù)是異步執(zhí)行,所以 2 最后輸出

頁面的背景色直接變成黑色,沒有經(jīng)過藍色的階段,是因為,我們在宏任務(wù)中將背景設(shè)置為藍色,但在進行渲染前執(zhí)行了微任務(wù), 在微任務(wù)中將背景變成了黑色,然后才執(zhí)行的渲染

第二個例子:

setTimeout(()?=>?{
?console.log(1)
?Promise.resolve(3).then(data?=>?console.log(data))
},?0)
setTimeout(()?=>?{
?console.log(2)
},?0)
//?print?:?1?3?2
復制代碼

上面代碼共包含兩個 setTimeout ,也就是說除主代碼塊外,共有兩個宏任務(wù), 其中第一個宏任務(wù)執(zhí)行中,輸出 1 ,并且創(chuàng)建了微任務(wù)隊列,所以在下一個宏任務(wù)隊列執(zhí)行前, 先執(zhí)行微任務(wù),在微任務(wù)執(zhí)行中,輸出 3 ,微任務(wù)執(zhí)行后,執(zhí)行下一次宏任務(wù),執(zhí)行中輸出 2

總結(jié)

  • 執(zhí)行一個宏任務(wù)(棧中沒有就從事件隊列中獲?。?/p>

  • 執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊列中

  • 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當前微任務(wù)隊列中的所有微任務(wù)(依次執(zhí)行)

  • 當前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染

  • 渲染完畢后,JS線程繼續(xù)接管,開始下一個宏任務(wù)(從事件隊列中獲取)

「前端進階」從多線程到Event Loop全面梳理


參考

  • WebKit技術(shù)內(nèi)幕

  • 瀏覽器都包含哪些進程

  • 進程與線程的一個簡單解釋

寫在最后

  • 文中如有錯誤,歡迎在評論區(qū)指正,如果這篇文章幫到了你,歡迎點贊和關(guān)注

  • 本文同步首發(fā)與github,可在github中找到更多精品文章,歡迎Watch & Star ★

「前端進階」從多線程到Event Loop全面梳理


要有更多資料視頻,請加小可樂微信

「前端進階」從多線程到Event Loop全面梳理


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI