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

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

假定工廠(chǎng)的電力有限,一次只能供給一個(gè)車(chē)間使用。 也就是說(shuō),一個(gè)車(chē)間開(kāi)工的時(shí)候,其他車(chē)間都必須停工。 背后的含義就是,單個(gè)CPU一次只能運(yùn)行一個(gè)任務(wù)。
進(jìn)程就好比工廠(chǎng)的車(chē)間,它代表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)程。
線(xiàn)程

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

在前端開(kāi)發(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引擎線(xiàn)程通知定時(shí)觸發(fā)器線(xiàn)程,間隔一個(gè)時(shí)間后,會(huì)觸發(fā)一個(gè)回調(diào)事件, 而定時(shí)觸發(fā)器線(xiàn)程在接收到這個(gè)消息后,會(huì)在等待的時(shí)間后,將回調(diào)事件放入到由事件觸發(fā)線(xiàn)程所管理的事件隊(duì)列中。
當(dāng)代碼執(zhí)行到XHR/fetch時(shí),實(shí)際上是JS引擎線(xiàn)程通知異步http請(qǐng)求線(xiàn)程,發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求,并制定請(qǐng)求完成后的回調(diào)事件, 而異步http請(qǐng)求線(xiàn)程在接收到這個(gè)消息后,會(huì)在請(qǐng)求成功后,將回調(diào)事件放入到由事件觸發(fā)線(xiàn)程所管理的事件隊(duì)列中。
當(dāng)我們的同步任務(wù)執(zhí)行完,JS引擎線(xiàn)程會(huì)詢(xún)問(wèn)事件觸發(fā)線(xiàn)程,在事件隊(duì)列中是否有待執(zhí)行的回調(diào)函數(shù),如果有就會(huì)加入到執(zhí)行棧中交給JS引擎線(xiàn)程執(zhí)行
用一張圖來(lái)解釋?zhuān)?/p>

再用代碼來(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í)器線(xiàn)程 1s 后將 timerCallback 交由事件觸發(fā)線(xiàn)程處理
// 1s 后事件觸發(fā)線(xiàn)程將 timerCallback 加入到事件隊(duì)列中
setTimeout(timerCallback,1000);
// 同步任務(wù)
// 通知異步http請(qǐng)求線(xiàn)程發(fā)送網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求成功后將 httpCallback 交由事件觸發(fā)線(xiàn)程處理
// 請(qǐng)求成功后事件觸發(fā)線(xiàn)程將 httpCallback 加入到事件隊(duì)列中
$.get('www.xxxx.com',httpCallback);
// 同步任務(wù)
console.log('world');
//...
// 所有同步任務(wù)執(zhí)行完后
// 詢(xún)問(wèn)事件觸發(fā)線(xiàn)程在事件事件隊(duì)列中是否有需要執(zhí)行的回調(diào)函數(shù)
// 如果沒(méi)有,一直詢(xún)問(wèn),直到有為止
// 如果有,將回調(diào)事件加入執(zhí)行棧中,開(kāi)始執(zhí)行回調(diào)代碼總結(jié)一下:
- JS引擎線(xiàn)程只執(zhí)行執(zhí)行棧中的事件
- 執(zhí)行棧中的代碼執(zhí)行完畢,就會(huì)讀取事件隊(duì)列中的事件
- 事件隊(duì)列中的回調(diào)事件,是由各自線(xiàn)程插入到事件隊(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引擎線(xiàn)程和GUI渲染線(xiàn)程是互斥的關(guān)系,瀏覽器為了能夠使宏任務(wù)和DOM任務(wù)有序的進(jìn)行,會(huì)在一個(gè)宏任務(wù)執(zhí)行結(jié)果后,在下一個(gè)宏任務(wù)執(zhí)行前,GUI渲染線(xiàn)程開(kāi)始工作,對(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線(xiàn)程會(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ì)列中獲取)
- 執(zhí)行過(guò)程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
- 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
- 當(dāng)前宏任務(wù)執(zhí)行完畢,開(kāi)始檢查渲染,然后GUI線(xiàn)程接管渲染
- 渲染完畢后,JS線(xiàn)程繼續(xù)接管,開(kāi)始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲?。?/li>

以上就是JS進(jìn)階之從多線(xiàn)程到Event Loop全面梳理的詳細(xì)內(nèi)容,更多關(guān)于JS Event Loop的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js實(shí)現(xiàn)點(diǎn)擊添加一個(gè)input節(jié)點(diǎn)
本文給大家分享的是一段點(diǎn)擊自動(dòng)添加inpu節(jié)點(diǎn)的代碼,非常的簡(jiǎn)單實(shí)用,這里推薦給大家。2014-12-12
js實(shí)現(xiàn)購(gòu)物車(chē)商品數(shù)量加減
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)購(gòu)物車(chē)商品數(shù)量加減,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09
layui+ssm實(shí)現(xiàn)數(shù)據(jù)批量刪除功能
本篇文章給大家介紹layui+ssm實(shí)現(xiàn)數(shù)據(jù)批量刪除功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-12-12
JavaScript使用prototype屬性實(shí)現(xiàn)繼承操作示例
這篇文章主要介紹了JavaScript使用prototype屬性實(shí)現(xiàn)繼承操作,結(jié)合實(shí)例形式詳細(xì)分析了JavaScript使用prototype屬性實(shí)現(xiàn)繼承的相關(guān)原理、實(shí)現(xiàn)方法與操作注意事項(xiàng),需要的朋友可以參考下2020-05-05
JS/HTML5游戲常用算法之碰撞檢測(cè) 像素檢測(cè)算法實(shí)例詳解
這篇文章主要介紹了JS/HTML5游戲常用算法之碰撞檢測(cè) 像素檢測(cè)算法,結(jié)合實(shí)例形式詳細(xì)分析了javascript像素檢測(cè)碰撞算法的原理、實(shí)現(xiàn)步驟及相關(guān)操作技巧,需要的朋友可以參考下2018-12-12

