前言
對于單線程來說糯钙,事件循環(huán)可以說是重中之重了,它為任務(wù)分配不同的優(yōu)先級退腥,井然有序的調(diào)度任岸。讓js解析,用戶交互狡刘,頁面渲染等互不沖突享潜,各司其職。
我們書寫的代碼無時無刻都在和事件循環(huán)打交道嗅蔬,要想寫出更流暢米碰,我們就必須深入了解事件循環(huán),下面我們將從規(guī)范中翻譯和解讀整個流程购城。
以下內(nèi)容來自whatwg文檔吕座,均為個人理解,若有不對瘪板,煩請指出吴趴,我會第一時間修改,避免誤導(dǎo)他人侮攀!
正文
為了協(xié)調(diào)用戶操作锣枝,js執(zhí)行,頁面渲染兰英,網(wǎng)絡(luò)請求等事件撇叁,每個宿主中,存在事件循環(huán)這樣的角色畦贸,并且該角色在當(dāng)前宿主中是唯一的陨闹。
簡單解釋一下宿主:宿主是一個ECMAScript執(zhí)行上下文,一般包含執(zhí)行上下文棧薄坏,運行時執(zhí)行環(huán)境趋厉,宿主記錄和一個執(zhí)行線程,除了這個執(zhí)行線程外胶坠,其他的專屬于當(dāng)前宿主君账。例如,某些瀏覽器在不同的tabs使用同一個執(zhí)行線程沈善。
不僅如此乡数,事件循環(huán)又存于在各個不同場景椭蹄,有瀏覽器環(huán)境下的,worker環(huán)境下的和Worklet環(huán)境下的净赴。
Worklet是一個輕量級的web worker塑娇,可以讓開發(fā)者訪問更底層的渲染工作線,也就是說你可以通過Worklet去干預(yù)瀏覽器的渲染環(huán)境劫侧。
提到了worklet埋酬,那就順便看一個例子(需開啟服務(wù),不要以file協(xié)議運行)烧栋,通過這個例子写妥,可以看到事件循環(huán)不同階段觸發(fā)了什么鉤子函數(shù):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.fancy {
background-image: paint(headerHighlight);
display: layout(sample-layout);
background-color: green;
}
</style>
</head>
<body>
<h1 class="fancy">My Cool Header</h1>
<script>
console.log('開始');
CSS.paintWorklet.addModule('./paint.js');
CSS.layoutWorklet.addModule('./layout.js');
requestAnimationFrame(() => {
console.log('requestAnimationFrame');
});
Promise.resolve().then(() => {
console.log('微任務(wù)');
});
setTimeout(function () {
document.querySelector('.fancy').style.height = '150px';
('translateZ(0)');
Promise.resolve().then(() => {
console.log('新一輪的微任務(wù)');
});
requestAnimationFrame(() => {
console.log('新一輪的requestAnimationFrame');
});
}, 2000);
console.log(2);
</script>
</body>
</html>
// paint.js
registerPaint(
'headerHighlight',
class {
static get contextOptions() {
console.log('contextOptions');
return {alpha: true};
}
paint(ctx) {
console.log('paint函數(shù)');
}
}
);
// ==========================分割線
// layout.js
registerLayout(
'sample-layout',
class {
async intrinsicSizes(children, edges, styleMap) {}
async layout(children, edges, constraints, styleMap, breakToken) {
console.log('layout階段');
}
}
);
事件循環(huán)有一個或多個Task隊列,每個Task隊列都是Task的一個集合审姓。其中Task不是指我們的某個函數(shù)珍特,而是一個上下文環(huán)境,結(jié)構(gòu)如下:
- step:一系列任務(wù)將要執(zhí)行的步驟
- source:任務(wù)來源魔吐,常用來對相關(guān)任務(wù)進(jìn)行分組和系列化
- document:與當(dāng)前任務(wù)相關(guān)的document對象扎筒,如果是非window環(huán)境則為null
- 環(huán)境配置對象:在任務(wù)期間追蹤記錄任務(wù)狀態(tài)
這里的Task隊列不是Task,是一個集合酬姆,因為取出一個Task隊列中的Task是選擇一個可執(zhí)行的Task嗜桌,而不是出隊操作。
微任務(wù)隊列是一個入對出對的隊列辞色。
這里說明一下骨宠,Task隊列為什么有多個爱只,因為不同的Task隊列有不同的優(yōu)先級穆刻,進(jìn)而進(jìn)行次序排列和調(diào)用熊尉,有沒有感覺react的fiber和這個有點類似浊洞?
舉個例子,Task隊列可以是專門負(fù)責(zé)鼠標(biāo)和鍵盤事件的盾似,并且賦予鼠標(biāo)鍵盤隊列較高的優(yōu)先級挽唉,以便及時響應(yīng)用戶操作塘秦。另一個Task隊列負(fù)責(zé)其他任務(wù)源建蹄。不過也不要餓死任何一個task碌更,這個后續(xù)處理模型中會介紹。
Task封裝了負(fù)責(zé)以下任務(wù)的算法:
- Events: 由專門的Task在特定的EventTarget(一個具有監(jiān)聽訂閱模式列表的對象)上分發(fā)事件對象
- Parsing: html解析器標(biāo)記一個或多個字節(jié)躲撰,并處理所有生成的結(jié)果token
- Callbacks: 由專門的Task觸發(fā)回調(diào)函數(shù)
- Using a resource: 當(dāng)該算法獲取資源的時候针贬,如果該階段是以非阻塞方式發(fā)生,那么一旦部分或者全部資源可用拢蛋,則由Task進(jìn)行后續(xù)處理
- Reacting to DOM manipulation: 通過dom操作觸發(fā)的任務(wù),例如插入一個節(jié)點到document
事件循環(huán)有一個當(dāng)前運行中的Task蔫巩,可以為null谆棱,如果是null的話快压,代表著可以接受一個新的Task(新一輪的步驟)。
事件循環(huán)有微任務(wù)隊列垃瞧,默認(rèn)為空蔫劣,其中的任務(wù)由微任務(wù)排隊算法創(chuàng)建。
事件循環(huán)有一個執(zhí)行微任務(wù)檢查點个从,默認(rèn)為false脉幢,用來防止微任務(wù)死循環(huán)。
- 如果未提供event loop嗦锐,設(shè)置一個隱式event loop嫌松。
- 如果未提供document,設(shè)置一個隱式document.
- 創(chuàng)建一個Task作為新的微任務(wù)
- 設(shè)置setp奕污、source萎羔、document到新的Task上
- 設(shè)置Task的環(huán)境配置對象為空集
- 添加到event loop的微任務(wù)隊列中
微任務(wù)檢查算法:
- 如果微任務(wù)檢查標(biāo)志為true,直接return
- 設(shè)置微任務(wù)檢查標(biāo)志為true
- 如果微任務(wù)隊里不為空(也就是說微任務(wù)添加的微任務(wù)也會在這個循環(huán)中出現(xiàn)碳默,直到微任務(wù)隊列為空):
- 從微任務(wù)隊列中找出最老的任務(wù)(防餓死)
- 設(shè)置當(dāng)前執(zhí)行任務(wù)為這個最老的任務(wù)
- 執(zhí)行
- 重置當(dāng)前執(zhí)行任務(wù)為null
- 通知環(huán)境配置對象的promise進(jìn)行reject操作
- 清理indexdb事務(wù)(不太明白這一步贾陷,如果有讀者了解,煩請點撥一下)
- 設(shè)置微任務(wù)檢查標(biāo)志為false
處理模型
event loop會按照下面這些步驟進(jìn)行調(diào)度:
- 找到一個可執(zhí)行的Task隊列嘱根,如果沒有則跳轉(zhuǎn)到下面的微任務(wù)步驟
- 讓最老的Task作為Task隊列中第一個可執(zhí)行的Task髓废,并將其移除
- 將最老的Task作為event loop的可執(zhí)行Task
- 記錄任務(wù)開始時間點
- 執(zhí)行Task中的setp對應(yīng)的步驟(上文中Task結(jié)構(gòu)中的step)
- 設(shè)置event loop的可執(zhí)行任務(wù)為null
- 執(zhí)行微任務(wù)檢查算法
- 設(shè)置hasARenderingOpportunity(是否可以渲染的flag)為false
- 記住當(dāng)前時間點
- 通過下面步驟記錄任務(wù)持續(xù)時間
- 設(shè)置頂層瀏覽器環(huán)境為空
- 對于每個最老Task的腳本執(zhí)行環(huán)境配置對象,設(shè)置當(dāng)前的頂級瀏覽器上下文到其上
- 報告消耗過長的任務(wù)该抒,并附帶開始時間瓦哎,結(jié)束時間,頂級瀏覽器上下文和當(dāng)前Task
- 如果在window環(huán)境下柔逼,會根據(jù)硬件條件決定是否渲染蒋譬,比如刷新率,頁面性能愉适,頁面是否在后臺犯助,不過渲染會定期出現(xiàn),避免頁面卡頓维咸。值得注意的是剂买,正常的刷新率為60hz,大概是每秒60幀癌蓖,大約16.7ms每幀瞬哼,如果當(dāng)前瀏覽器環(huán)境不支持這個刷新率的話,會自動降為30hz租副,而不是丟幀坐慰。而李蘭其在后臺的時候,聰明的瀏覽器會將這個渲染時機降為每秒4幀甚至更低用僧,事件循環(huán)也會減少(這就是為什么我們可以用setInterval來判斷時候能打開其他app的判斷依據(jù)的原因)结胀。如果能渲染的話會設(shè)置hasARenderingOpportunity為true赞咙。
除此之外,還會在觸發(fā)resize糟港、scroll攀操、建立媒體查詢、運行css動畫等秸抚,也就是說瀏覽器幾乎大部分用戶操作都發(fā)生在事件循環(huán)中速和,更具體點是事件循環(huán)中的ui render部分。之后會進(jìn)行requestAnimationFrame和IntersectionObserver的觸發(fā)剥汤,再之后是ui渲染
- 如果下面條件都成立颠放,那么執(zhí)行空閑階段算法,對于開發(fā)者來說就是調(diào)用window.requestIdleCallback方法
- 在window環(huán)境下
- event loop中沒有活躍的Task
- 微任務(wù)隊列為空
- hasARenderingOpportunity為false
借鑒網(wǎng)上的一張圖來粗略表示下整個流程
小結(jié)
上面就是整個事件循環(huán)的流程秀姐,瀏覽器就是按照這個規(guī)則一遍遍的執(zhí)行慈迈,而我們要做的就是了解并適應(yīng)這個規(guī)則,讓瀏覽器渲染出性能更高的頁面省有。
比如:
- 非首屏相關(guān)性能打點可以放到idle callback中執(zhí)行痒留,減少對頁面性能的損耗
- 微任務(wù)中遞歸添加微任務(wù)會導(dǎo)致頁面卡死,而不是隨著事件循環(huán)一輪輪的執(zhí)行
- 更新元素布局的最好時機是在requestAnimateFrame中
- 盡量避免頻繁獲取元素布局信息蠢沿,因為這會觸發(fā)強制layout(哪些屬性會導(dǎo)致強制layout伸头?),影響頁面性能
- 事件循環(huán)有多個任務(wù)隊列舷蟀,他們互不沖突恤磷,但是用戶交互相關(guān)的優(yōu)先級更高
- resize、scroll等會伴隨事件循環(huán)中ui渲染觸發(fā)野宜,而不是根據(jù)我們的滾動觸發(fā)扫步,換句話說,這些操作自帶節(jié)流
- 等等匈子,歡迎補充
最后感謝大家閱讀河胎,歡迎一起探討!