JS進(jìn)階之從多線程到Event?Loop全面梳理
引子
幾乎在每一本JS相關(guān)的書籍中,都會(huì)說(shuō)JS是單線程的,JS是通過(guò)事件隊(duì)列(Event Loop)的方式來(lái)實(shí)現(xiàn)異步回調(diào)的。 對(duì)很多初學(xué)JS的人來(lái)說(shuō),根本搞不清楚單線程的JS為什么擁有異步的能力,所以,我試圖從進(jìn)程、線程的角度來(lái)解釋這個(gè)問(wèn)題。
CPU

計(jì)算機(jī)的核心是CPU,它承擔(dān)了所有的計(jì)算任務(wù)。
它就像一座工廠,時(shí)刻在運(yùn)行。
進(jìn)程

假定工廠的電力有限,一次只能供給一個(gè)車間使用。 也就是說(shuō),一個(gè)車間開工的時(shí)候,其他車間都必須停工。 背后的含義就是,單個(gè)CPU一次只能運(yùn)行一個(gè)任務(wù)。
進(jìn)程就好比工廠的車間,它代表CPU所能處理的單個(gè)任務(wù)。 進(jìn)程之間相互獨(dú)立,任一時(shí)刻,CPU總是運(yùn)行一個(gè)進(jìn)程,其他進(jìn)程處于非運(yùn)行狀態(tài)。 CPU使用時(shí)間片輪轉(zhuǎn)進(jìn)度算法來(lái)實(shí)現(xiàn)同時(shí)運(yùn)行多個(gè)進(jìn)程。
線程

一個(gè)車間里,可以有很多工人,共享車間所有的資源,他們協(xié)同完成一個(gè)任務(wù)。
線程就好比車間里的工人,一個(gè)進(jìn)程可以包括多個(gè)線程,多個(gè)線程共享進(jìn)程資源。
CPU、進(jìn)程、線程之間的關(guān)系
從上文我們已經(jīng)簡(jiǎn)單了解了CPU、進(jìn)程、線程,簡(jiǎn)單匯總一下。
- 進(jìn)程是cpu資源分配的最小單位(是能擁有資源和獨(dú)立運(yùn)行的最小單位)
- 線程是cpu調(diào)度的最小單位(線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運(yùn)行單位,一個(gè)進(jìn)程中可以有多個(gè)線程)
- 不同進(jìn)程之間也可以通信,不過(guò)代價(jià)較大
- 單線程與多線程,都是指在一個(gè)進(jìn)程內(nèi)的單和多
瀏覽器是多進(jìn)程的
我們已經(jīng)知道了CPU、進(jìn)程、線程之間的關(guān)系,對(duì)于計(jì)算機(jī)來(lái)說(shuō),每一個(gè)應(yīng)用程序都是一個(gè)進(jìn)程, 而每一個(gè)應(yīng)用程序都會(huì)分別有很多的功能模塊,這些功能模塊實(shí)際上是通過(guò)子進(jìn)程來(lái)實(shí)現(xiàn)的。 對(duì)于這種子進(jìn)程的擴(kuò)展方式,我們可以稱這個(gè)應(yīng)用程序是多進(jìn)程的。
而對(duì)于瀏覽器來(lái)說(shuō),瀏覽器就是多進(jìn)程的,我在Chrome瀏覽器中打開了多個(gè)tab,然后打開windows控制管理器:

如上圖,我們可以看到一個(gè)Chrome瀏覽器啟動(dòng)了好多個(gè)進(jìn)程。
總結(jié)一下:
- 瀏覽器是多進(jìn)程的
- 每一個(gè)Tab頁(yè),就是一個(gè)獨(dú)立的進(jìn)程
瀏覽器包含了哪些進(jìn)程
主進(jìn)程
- 協(xié)調(diào)控制其他子進(jìn)程(創(chuàng)建、銷毀)
- 瀏覽器界面顯示,用戶交互,前進(jìn)、后退、收藏
- 將渲染進(jìn)程得到的內(nèi)存中的Bitmap,繪制到用戶界面上
- 處理不可見(jiàn)操作,網(wǎng)絡(luò)請(qǐng)求,文件訪問(wèn)等
第三方插件進(jìn)程
每種類型的插件對(duì)應(yīng)一個(gè)進(jìn)程,僅當(dāng)使用該插件時(shí)才創(chuàng)建
GPU進(jìn)程
用于3D繪制等
渲染進(jìn)程,就是我們說(shuō)的瀏覽器內(nèi)核
- 負(fù)責(zé)頁(yè)面渲染,腳本執(zhí)行,事件處理等
- 每個(gè)tab頁(yè)一個(gè)渲染進(jìn)程
那么瀏覽器中包含了這么多的進(jìn)程,那么對(duì)于普通的前端操作來(lái)說(shuō),最重要的是什么呢?
答案是渲染進(jìn)程,也就是我們常說(shuō)的瀏覽器內(nèi)核
瀏覽器內(nèi)核(渲染進(jìn)程)
從前文我們得知,進(jìn)程和線程是一對(duì)多的關(guān)系,也就是說(shuō)一個(gè)進(jìn)程包含了多條線程。
而對(duì)于渲染進(jìn)程來(lái)說(shuō),它當(dāng)然也是多線程的了,接下來(lái)我們來(lái)看一下渲染進(jìn)程包含哪些線程。
GUI渲染線程
- 負(fù)責(zé)渲染頁(yè)面,布局和繪制
- 頁(yè)面需要重繪和回流時(shí),該線程就會(huì)執(zhí)行
- 與js引擎線程互斥,防止渲染結(jié)果不可預(yù)期
JS引擎線程
- 負(fù)責(zé)處理解析和執(zhí)行javascript腳本程序
- 只有一個(gè)JS引擎線程(單線程)
- 與GUI渲染線程互斥,防止渲染結(jié)果不可預(yù)期
事件觸發(fā)線程
- 用來(lái)控制事件循環(huán)(鼠標(biāo)點(diǎn)擊、setTimeout、ajax等)
- 當(dāng)事件滿足觸發(fā)條件時(shí),將事件放入到JS引擎所在的執(zhí)行隊(duì)列中
定時(shí)觸發(fā)器線程
- setInterval與setTimeout所在的線程
- 定時(shí)任務(wù)并不是由JS引擎計(jì)時(shí)的,是由定時(shí)觸發(fā)線程來(lái)計(jì)時(shí)的
- 計(jì)時(shí)完畢后,通知事件觸發(fā)線程
異步http請(qǐng)求線程
- 瀏覽器有一個(gè)單獨(dú)的線程用于處理AJAX請(qǐng)求
- 當(dāng)請(qǐng)求完成時(shí),若有回調(diào)函數(shù),通知事件觸發(fā)線程
當(dāng)我們了解了渲染進(jìn)程包含的這些線程后,我們思考兩個(gè)問(wèn)題:
- 為什么 javascript 是單線程的
- 為什么 GUI 渲染線程為什么與 JS 引擎線程互斥
為什么 javascript 是單線程的
首先是歷史原因,在創(chuàng)建 javascript 這門語(yǔ)言時(shí),多進(jìn)程多線程的架構(gòu)并不流行,硬件支持并不好。
其次是因?yàn)槎嗑€程的復(fù)雜性,多線程操作需要加鎖,編碼的復(fù)雜性會(huì)增高。
而且,如果同時(shí)操作 DOM ,在多線程不加鎖的情況下,最終會(huì)導(dǎo)致 DOM 渲染的結(jié)果不可預(yù)期。
為什么 GUI 渲染線程與 JS 引擎線程互斥
這是由于 JS 是可以操作 DOM 的,如果同時(shí)修改元素屬性并同時(shí)渲染界面(即 JS線程和UI線程同時(shí)運(yùn)行), 那么渲染線程前后獲得的元素就可能不一致了。
因此,為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)定 GUI渲染線程和JS引擎線程為互斥關(guān)系, 當(dāng)JS引擎線程執(zhí)行時(shí)GUI渲染線程會(huì)被掛起,GUI更新則會(huì)被保存在一個(gè)隊(duì)列中等待JS引擎線程空閑時(shí)立即被執(zhí)行。
從 Event Loop 看 JS 的運(yùn)行機(jī)制
到了這里,終于要進(jìn)入我們的主題,什么是 Event Loop
先理解一些概念:
- JS 分為同步任務(wù)和異步任務(wù)
- 同步任務(wù)都在JS引擎線程上執(zhí)行,形成一個(gè)執(zhí)行棧
- 事件觸發(fā)線程管理一個(gè)任務(wù)隊(duì)列,異步任務(wù)觸發(fā)條件達(dá)成,將回調(diào)事件放到任務(wù)隊(duì)列中
- 執(zhí)行棧中所有同步任務(wù)執(zhí)行完畢,此時(shí)JS引擎線程空閑,系統(tǒng)會(huì)讀取任務(wù)隊(duì)列,將可運(yùn)行的異步任務(wù)回調(diào)事件添加到執(zhí)行棧中,開始執(zhí)行

在前端開發(fā)中我們會(huì)通過(guò)setTimeout/setInterval來(lái)指定定時(shí)任務(wù),會(huì)通過(guò)XHR/fetch發(fā)送網(wǎng)絡(luò)請(qǐng)求, 接下來(lái)簡(jiǎn)述一下setTimeout/setInterval和XHR/fetch到底做了什么事
我們知道,不管是setTimeout/setInterval和XHR/fetch代碼,在這些代碼執(zhí)行時(shí), 本身是同步任務(wù),而其中的回調(diào)函數(shù)才是異步任務(wù)。
當(dāng)代碼執(zhí)行到setTimeout/setInterval時(shí),實(shí)際上是JS引擎線程通知定時(shí)觸發(fā)器線程,間隔一個(gè)時(shí)間后,會(huì)觸發(fā)一個(gè)回調(diào)事件, 而定時(shí)觸發(fā)器線程在接收到這個(gè)消息后,會(huì)在等待的時(shí)間后,將回調(diào)事件放入到由事件觸發(fā)線程所管理的事件隊(duì)列中。
當(dāng)代碼執(zhí)行到XHR/fetch時(shí),實(shí)際上是JS引擎線程通知異步http請(qǐng)求線程,發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求,并制定請(qǐng)求完成后的回調(diào)事件, 而異步http請(qǐng)求線程在接收到這個(gè)消息后,會(huì)在請(qǐng)求成功后,將回調(diào)事件放入到由事件觸發(fā)線程所管理的事件隊(duì)列中。
當(dāng)我們的同步任務(wù)執(zhí)行完,JS引擎線程會(huì)詢問(wèn)事件觸發(fā)線程,在事件隊(duì)列中是否有待執(zhí)行的回調(diào)函數(shù),如果有就會(huì)加入到執(zhí)行棧中交給JS引擎線程執(zhí)行
用一張圖來(lái)解釋:

再用代碼來(lái)解釋一下:
let timerCallback = function() {
console.log('wait one second');
};
let httpCallback = function() {
console.log('get server data success');
}
// 同步任務(wù)
console.log('hello');
// 同步任務(wù)
// 通知定時(shí)器線程 1s 后將 timerCallback 交由事件觸發(fā)線程處理
// 1s 后事件觸發(fā)線程將 timerCallback 加入到事件隊(duì)列中
setTimeout(timerCallback,1000);
// 同步任務(wù)
// 通知異步http請(qǐng)求線程發(fā)送網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求成功后將 httpCallback 交由事件觸發(fā)線程處理
// 請(qǐng)求成功后事件觸發(fā)線程將 httpCallback 加入到事件隊(duì)列中
$.get('www.xxxx.com',httpCallback);
// 同步任務(wù)
console.log('world');
//...
// 所有同步任務(wù)執(zhí)行完后
// 詢問(wèn)事件觸發(fā)線程在事件事件隊(duì)列中是否有需要執(zhí)行的回調(diào)函數(shù)
// 如果沒(méi)有,一直詢問(wèn),直到有為止
// 如果有,將回調(diào)事件加入執(zhí)行棧中,開始執(zhí)行回調(diào)代碼總結(jié)一下:
- JS引擎線程只執(zhí)行執(zhí)行棧中的事件
- 執(zhí)行棧中的代碼執(zhí)行完畢,就會(huì)讀取事件隊(duì)列中的事件
- 事件隊(duì)列中的回調(diào)事件,是由各自線程插入到事件隊(duì)列中的
- 如此循環(huán)
宏任務(wù)、微任務(wù)
當(dāng)我們基本了解了什么是執(zhí)行棧,什么是事件隊(duì)列之后,我們深入了解一下事件循環(huán)中宏任務(wù)、微任務(wù)
什么是宏任務(wù)
我們可以將每次執(zhí)行棧執(zhí)行的代碼當(dāng)做是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行), 每一個(gè)宏任務(wù)會(huì)從頭到尾執(zhí)行完畢,不會(huì)執(zhí)行其他。
我們前文提到過(guò)JS引擎線程和GUI渲染線程是互斥的關(guān)系,瀏覽器為了能夠使宏任務(wù)和DOM任務(wù)有序的進(jìn)行,會(huì)在一個(gè)宏任務(wù)執(zhí)行結(jié)果后,在下一個(gè)宏任務(wù)執(zhí)行前,GUI渲染線程開始工作,對(duì)頁(yè)面進(jìn)行渲染。
// 宏任務(wù)-->渲染-->宏任務(wù)-->渲染-->渲染...
主代碼塊,setTimeout,setInterval等,都屬于宏任務(wù)
第一個(gè)例子:
document.body.style = 'background:black'; document.body.style = 'background:red'; document.body.style = 'background:blue'; document.body.style = 'background:grey';
我們可以將這段代碼放到瀏覽器的控制臺(tái)執(zhí)行以下,看一下效果:

我們會(huì)看到的結(jié)果是,頁(yè)面背景會(huì)在瞬間變成灰色,以上代碼屬于同一次宏任務(wù),所以全部執(zhí)行完才觸發(fā)頁(yè)面渲染,渲染時(shí)GUI線程會(huì)將所有UI改動(dòng)優(yōu)化合并,所以視覺(jué)效果上,只會(huì)看到頁(yè)面變成灰色。
第二個(gè)例子:
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)執(zhí)行一下,再看效果:

我會(huì)看到,頁(yè)面先顯示成藍(lán)色背景,然后瞬間變成了黑色背景,這是因?yàn)橐陨洗a屬于兩次宏任務(wù),第一次宏任務(wù)執(zhí)行的代碼是將背景變成藍(lán)色,然后觸發(fā)渲染,將頁(yè)面變成藍(lán)色,再觸發(fā)第二次宏任務(wù)將背景變成黑色。
什么是微任務(wù)
我們已經(jīng)知道宏任務(wù)結(jié)束后,會(huì)執(zhí)行渲染,然后執(zhí)行下一個(gè)宏任務(wù), 而微任務(wù)可以理解成在當(dāng)前宏任務(wù)執(zhí)行后立即執(zhí)行的任務(wù)。
也就是說(shuō),當(dāng)宏任務(wù)執(zhí)行完,會(huì)在渲染前,將執(zhí)行期間所產(chǎn)生的所有微任務(wù)都執(zhí)行完。
Promise,process.nextTick等,屬于微任務(wù)。
第一個(gè)例子:
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);執(zhí)行一下,再看效果:

控制臺(tái)輸出 1 3 2 , 是因?yàn)?promise 對(duì)象的 then 方法的回調(diào)函數(shù)是異步執(zhí)行,所以 2 最后輸出
頁(yè)面的背景色直接變成黑色,沒(méi)有經(jīng)過(guò)藍(lán)色的階段,是因?yàn)椋覀冊(cè)诤耆蝿?wù)中將背景設(shè)置為藍(lán)色,但在進(jìn)行渲染前執(zhí)行了微任務(wù), 在微任務(wù)中將背景變成了黑色,然后才執(zhí)行的渲染
第二個(gè)例子:
setTimeout(() => {
console.log(1)
Promise.resolve(3).then(data => console.log(data))
}, 0)
setTimeout(() => {
console.log(2)
}, 0)
// print : 1 3 2上面代碼共包含兩個(gè) setTimeout ,也就是說(shuō)除主代碼塊外,共有兩個(gè)宏任務(wù), 其中第一個(gè)宏任務(wù)執(zhí)行中,輸出 1 ,并且創(chuàng)建了微任務(wù)隊(duì)列,所以在下一個(gè)宏任務(wù)隊(duì)列執(zhí)行前, 先執(zhí)行微任務(wù),在微任務(wù)執(zhí)行中,輸出 3 ,微任務(wù)執(zhí)行后,執(zhí)行下一次宏任務(wù),執(zhí)行中輸出 2
總結(jié)
- 執(zhí)行一個(gè)宏任務(wù)(棧中沒(méi)有就從事件隊(duì)列中獲?。?/li>
- 執(zhí)行過(guò)程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
- 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
- 當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后,JS線程繼續(xù)接管,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲?。?/li>

以上就是JS進(jìn)階之從多線程到Event Loop全面梳理的詳細(xì)內(nèi)容,更多關(guān)于JS Event Loop的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS實(shí)現(xiàn)將圖片轉(zhuǎn)為base64格式
Base64是一種用64個(gè)字符來(lái)表示任意二進(jìn)制數(shù)據(jù)的方法,這篇文章主要為大家介紹了如何實(shí)現(xiàn)將圖片轉(zhuǎn)為base64格式,感興趣的小伙伴可以學(xué)習(xí)一下2023-07-07
js獲取上傳文件的絕對(duì)路徑實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇js獲取上傳文件的絕對(duì)路徑實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08
小程序顯示彈窗時(shí)禁止下層的內(nèi)容滾動(dòng)實(shí)現(xiàn)方法
這篇文章主要介紹了小程序顯示彈窗時(shí)禁止下層的內(nèi)容滾動(dòng)實(shí)現(xiàn)方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03
JS隨拖拽速度設(shè)置傾斜角度的實(shí)現(xiàn)代碼
這篇文章主要給大家介紹了JS如何隨拖拽速度設(shè)置傾斜角度,文中有詳細(xì)的代碼講解,對(duì)大家的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以自己動(dòng)手嘗試一下2023-09-09
ionic2打包android時(shí)gradle無(wú)法下載的解決方法
這篇文章主要為大家詳細(xì)介紹了ionic2打包android時(shí)gradle無(wú)法下載的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04

