JS進(jìn)階之從多線程到Event?Loop全面梳理
引子
幾乎在每一本JS相關(guān)的書籍中,都會說JS是單線程的,JS是通過事件隊(duì)列(Event Loop)
的方式來實(shí)現(xiàn)異步回調(diào)的。 對很多初學(xué)JS的人來說,根本搞不清楚單線程的JS為什么擁有異步的能力,所以,我試圖從進(jìn)程、線程的角度來解釋這個(gè)問題。
CPU
計(jì)算機(jī)的核心是CPU
,它承擔(dān)了所有的計(jì)算任務(wù)。
它就像一座工廠,時(shí)刻在運(yùn)行。
進(jìn)程
假定工廠的電力有限,一次只能供給一個(gè)車間使用。 也就是說,一個(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)度算法來實(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)簡單了解了CPU、進(jìn)程、線程,簡單匯總一下。
- 進(jìn)程是cpu資源分配的最小單位(是能擁有資源和獨(dú)立運(yùn)行的最小單位)
- 線程是cpu調(diào)度的最小單位(線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運(yùn)行單位,一個(gè)進(jìn)程中可以有多個(gè)線程)
- 不同進(jìn)程之間也可以通信,不過代價(jià)較大
- 單線程與多線程,都是指在一個(gè)進(jìn)程內(nèi)的單和多
瀏覽器是多進(jìn)程的
我們已經(jīng)知道了CPU
、進(jìn)程、線程之間的關(guān)系,對于計(jì)算機(jī)來說,每一個(gè)應(yīng)用程序都是一個(gè)進(jìn)程, 而每一個(gè)應(yīng)用程序都會分別有很多的功能模塊,這些功能模塊實(shí)際上是通過子進(jìn)程來實(shí)現(xiàn)的。 對于這種子進(jìn)程的擴(kuò)展方式,我們可以稱這個(gè)應(yīng)用程序是多進(jìn)程的。
而對于瀏覽器來說,瀏覽器就是多進(jìn)程的,我在Chrome瀏覽器中打開了多個(gè)tab,然后打開windows控制管理器:
如上圖,我們可以看到一個(gè)Chrome瀏覽器啟動了好多個(gè)進(jìn)程。
總結(jié)一下:
- 瀏覽器是多進(jìn)程的
- 每一個(gè)Tab頁,就是一個(gè)獨(dú)立的進(jìn)程
瀏覽器包含了哪些進(jìn)程
主進(jìn)程
- 協(xié)調(diào)控制其他子進(jìn)程(創(chuàng)建、銷毀)
- 瀏覽器界面顯示,用戶交互,前進(jìn)、后退、收藏
- 將渲染進(jìn)程得到的內(nèi)存中的Bitmap,繪制到用戶界面上
- 處理不可見操作,網(wǎng)絡(luò)請求,文件訪問等
第三方插件進(jìn)程
每種類型的插件對應(yīng)一個(gè)進(jìn)程,僅當(dāng)使用該插件時(shí)才創(chuàng)建
GPU進(jìn)程
用于3D繪制等
渲染進(jìn)程,就是我們說的瀏覽器內(nèi)核
- 負(fù)責(zé)頁面渲染,腳本執(zhí)行,事件處理等
- 每個(gè)tab頁一個(gè)渲染進(jìn)程
那么瀏覽器中包含了這么多的進(jìn)程,那么對于普通的前端操作來說,最重要的是什么呢?
答案是渲染進(jìn)程,也就是我們常說的瀏覽器內(nèi)核
瀏覽器內(nèi)核(渲染進(jìn)程)
從前文我們得知,進(jìn)程和線程是一對多的關(guān)系,也就是說一個(gè)進(jìn)程包含了多條線程。
而對于渲染進(jìn)程來說,它當(dāng)然也是多線程的了,接下來我們來看一下渲染進(jìn)程包含哪些線程。
GUI渲染線程
- 負(fù)責(zé)渲染頁面,布局和繪制
- 頁面需要重繪和回流時(shí),該線程就會執(zhí)行
- 與js引擎線程互斥,防止渲染結(jié)果不可預(yù)期
JS引擎線程
- 負(fù)責(zé)處理解析和執(zhí)行javascript腳本程序
- 只有一個(gè)JS引擎線程(單線程)
- 與GUI渲染線程互斥,防止渲染結(jié)果不可預(yù)期
事件觸發(fā)線程
- 用來控制事件循環(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ā)線程來計(jì)時(shí)的
- 計(jì)時(shí)完畢后,通知事件觸發(fā)線程
異步http請求線程
- 瀏覽器有一個(gè)單獨(dú)的線程用于處理AJAX請求
- 當(dāng)請求完成時(shí),若有回調(diào)函數(shù),通知事件觸發(fā)線程
當(dāng)我們了解了渲染進(jìn)程包含的這些線程后,我們思考兩個(gè)問題:
- 為什么 javascript 是單線程的
- 為什么 GUI 渲染線程為什么與 JS 引擎線程互斥
為什么 javascript 是單線程的
首先是歷史原因,在創(chuàng)建 javascript 這門語言時(shí),多進(jìn)程多線程的架構(gòu)并不流行,硬件支持并不好。
其次是因?yàn)槎嗑€程的復(fù)雜性,多線程操作需要加鎖,編碼的復(fù)雜性會增高。
而且,如果同時(shí)操作 DOM ,在多線程不加鎖的情況下,最終會導(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渲染線程會被掛起,GUI更新則會被保存在一個(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)會讀取任務(wù)隊(duì)列,將可運(yùn)行的異步任務(wù)回調(diào)事件添加到執(zhí)行棧中,開始執(zhí)行
在前端開發(fā)中我們會通過setTimeout/setInterval
來指定定時(shí)任務(wù),會通過XHR/fetch
發(fā)送網(wǎng)絡(luò)請求, 接下來簡述一下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í)間后,會觸發(fā)一個(gè)回調(diào)事件, 而定時(shí)觸發(fā)器線程在接收到這個(gè)消息后,會在等待的時(shí)間后,將回調(diào)事件放入到由事件觸發(fā)線程所管理的事件隊(duì)列中。
當(dāng)代碼執(zhí)行到XHR/fetch
時(shí),實(shí)際上是JS引擎線程通知異步http請求線程,發(fā)送一個(gè)網(wǎng)絡(luò)請求,并制定請求完成后的回調(diào)事件, 而異步http請求線程在接收到這個(gè)消息后,會在請求成功后,將回調(diào)事件放入到由事件觸發(fā)線程所管理的事件隊(duì)列中。
當(dāng)我們的同步任務(wù)執(zhí)行完,JS引擎線程會詢問事件觸發(fā)線程,在事件隊(duì)列中是否有待執(zhí)行的回調(diào)函數(shù),如果有就會加入到執(zhí)行棧中交給JS引擎線程執(zhí)行
用一張圖來解釋:
再用代碼來解釋一下:
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請求線程發(fā)送網(wǎng)絡(luò)請求,請求成功后將 httpCallback 交由事件觸發(fā)線程處理 // 請求成功后事件觸發(fā)線程將 httpCallback 加入到事件隊(duì)列中 $.get('www.xxxx.com',httpCallback); // 同步任務(wù) console.log('world'); //... // 所有同步任務(wù)執(zhí)行完后 // 詢問事件觸發(fā)線程在事件事件隊(duì)列中是否有需要執(zhí)行的回調(diào)函數(shù) // 如果沒有,一直詢問,直到有為止 // 如果有,將回調(diào)事件加入執(zhí)行棧中,開始執(zhí)行回調(diào)代碼
總結(jié)一下:
- JS引擎線程只執(zhí)行執(zhí)行棧中的事件
- 執(zhí)行棧中的代碼執(zhí)行完畢,就會讀取事件隊(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ù)會從頭到尾執(zhí)行完畢,不會執(zhí)行其他。
我們前文提到過JS引擎線程和GUI渲染線程是互斥的關(guān)系,瀏覽器為了能夠使宏任務(wù)和DOM任務(wù)有序的進(jìn)行,會在一個(gè)宏任務(wù)執(zhí)行結(jié)果后,在下一個(gè)宏任務(wù)執(zhí)行前,GUI渲染線程開始工作,對頁面進(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';
我們可以將這段代碼放到瀏覽器的控制臺執(zhí)行以下,看一下效果:
我們會看到的結(jié)果是,頁面背景會在瞬間變成灰色,以上代碼屬于同一次宏任務(wù),所以全部執(zhí)行完才觸發(fā)頁面渲染,渲染時(shí)GUI線程會將所有UI改動優(yōu)化合并,所以視覺效果上,只會看到頁面變成灰色。
第二個(gè)例子:
document.body.style = 'background:blue'; setTimeout(function(){ document.body.style = 'background:black' },0)
執(zhí)行一下,再看效果:
我會看到,頁面先顯示成藍(lán)色背景,然后瞬間變成了黑色背景,這是因?yàn)橐陨洗a屬于兩次宏任務(wù),第一次宏任務(wù)執(zhí)行的代碼是將背景變成藍(lán)色,然后觸發(fā)渲染,將頁面變成藍(lán)色,再觸發(fā)第二次宏任務(wù)將背景變成黑色。
什么是微任務(wù)
我們已經(jīng)知道宏任務(wù)結(jié)束后,會執(zhí)行渲染,然后執(zhí)行下一個(gè)宏任務(wù), 而微任務(wù)可以理解成在當(dāng)前宏任務(wù)執(zhí)行后立即執(zhí)行的任務(wù)。
也就是說,當(dāng)宏任務(wù)執(zhí)行完,會在渲染前,將執(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í)行一下,再看效果:
控制臺輸出 1 3 2 , 是因?yàn)?promise 對象的 then 方法的回調(diào)函數(shù)是異步執(zhí)行,所以 2 最后輸出
頁面的背景色直接變成黑色,沒有經(jīng)過藍(lán)色的階段,是因?yàn)?,我們在宏任?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 ,也就是說除主代碼塊外,共有兩個(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ù)(棧中沒有就從事件隊(duì)列中獲?。?/li>
- 執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
- 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
- 當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后,JS線程繼續(xù)接管,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲取)
以上就是JS進(jìn)階之從多線程到Event Loop全面梳理的詳細(xì)內(nèi)容,更多關(guān)于JS Event Loop的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js實(shí)現(xiàn)點(diǎn)擊添加一個(gè)input節(jié)點(diǎn)
本文給大家分享的是一段點(diǎn)擊自動添加inpu節(jié)點(diǎn)的代碼,非常的簡單實(shí)用,這里推薦給大家。2014-12-12layui+ssm實(shí)現(xiàn)數(shù)據(jù)批量刪除功能
本篇文章給大家介紹layui+ssm實(shí)現(xiàn)數(shù)據(jù)批量刪除功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-12-12JavaScript使用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-05JS/HTML5游戲常用算法之碰撞檢測 像素檢測算法實(shí)例詳解
這篇文章主要介紹了JS/HTML5游戲常用算法之碰撞檢測 像素檢測算法,結(jié)合實(shí)例形式詳細(xì)分析了javascript像素檢測碰撞算法的原理、實(shí)現(xiàn)步驟及相關(guān)操作技巧,需要的朋友可以參考下2018-12-12