欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS進(jìn)階之從多線程到Event?Loop全面梳理

 更新時(shí)間:2023年05月23日 10:53:26   作者:云中橋  
JS是通過事件隊(duì)列(Event?Loop)的方式來實(shí)現(xiàn)異步回調(diào)的,但對很多初學(xué)JS的人來說,根本搞不清楚單線程的JS為什么擁有異步的能力,所以本文將從進(jìn)程、線程的角度來解釋這個(gè)問題

引子

幾乎在每一本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/setIntervalXHR/fetch到底做了什么事

我們知道,不管是setTimeout/setIntervalXHR/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)文章

最新評論