基于 Performace 分析事件循環(huán)

我們是袋鼠云數(shù)棧 UED 團隊,致力于打造優(yōu)秀的一站式數(shù)據(jù)中臺產(chǎn)品。我們始終保持工匠精神,探索前端道路,為社區(qū)積累并傳播經(jīng)驗價值。

本文作者:千尋

什么是事件循環(huán)?

我們?yōu)槭裁葱枰录h(huán)?對于 JavaScript 是一門單線程語言我們是肯定的,JavaScript 單線程的特性保證了渲染和 JavaScript 的正常運行,但同時也存在一定的限制。理想情況下我們希望所有任務(wù)是串行執(zhí)行的,假設(shè)串行中存在一個耗時很多的任務(wù)時,會阻塞后續(xù)任務(wù)的運行,這種情況我們怎么去解決呢?這個時候就需要我們的事件循環(huán)來處理了。

file

讓人意外的setTimeout

菜鳥教程:setTimeout() :在指定的毫秒數(shù)后調(diào)用函數(shù)或計算表達式

console.log(1); 
setTimeout(()=>{    
  console.log(2); 
},0) 
for (let i = 0; i < 5000; i++) {    
  let sum = 0;
  sum += i;  
} 
console.log(3); 

猜猜上面這段代碼執(zhí)行結(jié)果是多少呢?根據(jù) Event Loop 機制我們知道答案是1、3、2。但是針對這段代碼中有一個疑問點,0ms 是指 0ms 后執(zhí)行 callback 嗎?答案是否定的,定時器任務(wù)被維護在定時器線程中,添加一個定時器時開始計時這個任務(wù),0ms 后會將 callback 添加到事件隊列中,<script> 宏任務(wù)中循環(huán)計算導(dǎo)致耗時長,阻塞定時器的 callback 執(zhí)行,查看 Performance 執(zhí)行過程驗證猜想。

file

宏任務(wù)與微任務(wù)

我們先回顧一下之前 Event Loop 圖,在圖中描述了函數(shù)調(diào)用棧、Web Api、一個消息隊列,并沒有提到宏任務(wù)與微任務(wù),那么宏任務(wù)與微任務(wù)是什么呢?為什么要有微任務(wù)呢?我們先來看一個例子:

function timerCallback2(){      
  console.log(2); 
}  
function timerCallback(){      
  console.log(1);     
  setTimeout(timerCallback2,0); 
}  
setTimeout(timerCallback,0); 

我們希望通過 setTimeout 按照順序執(zhí)行 callback,通過 Performance 發(fā)現(xiàn),在兩個任務(wù)中間插入了其他任務(wù),如果插入任務(wù)是 long task,會影響后續(xù)任務(wù)的執(zhí)行。宏任務(wù)是瀏覽器提供給我們的Web Api,時間顆粒度較大,針對像 DOM 等高實時性操作是不太符合的。

file

為了滿足這種高優(yōu)先級的任務(wù),V8 引擎在創(chuàng)建全局執(zhí)行上下文時會在內(nèi)部創(chuàng)建一個微任務(wù)隊列,在當(dāng)前宏任務(wù)執(zhí)行完成時去檢查微任務(wù)隊列,我們把執(zhí)行微任務(wù)的時間點叫檢查點。了解了微任務(wù)隊列后,我們豐富一下之前的 Event Loop。

file
console.log(1);
new Promise(function (resolve) {   
  console.log(2);   
  resolve() 
}).then(function () {   
  console.log(3);
}) 
console.log(4);

事件循環(huán)與渲染

瀏覽器按照幀渲染方式一幀一幀渲染網(wǎng)頁,但并不是每一幀都會經(jīng)歷管道每個部分的處理。當(dāng)腳本執(zhí)行阻塞時會導(dǎo)致后續(xù)渲染流暢阻塞,頁面卡頓。

file

瀏覽器何時渲染對于我們來說就是一個黑盒,瀏覽器自身會去判斷當(dāng)前是否需要進行渲染,因此性能優(yōu)化的是管道幀的流水過程,比如減少腳本執(zhí)行時長,避免重繪、重排。如果你希望在每輪事件循環(huán)中都能變動,你需要去了解一下 requestAnimationFrame。

file
<div id='con'>this is con</div> 
<script> 
  var con = document.getElementById('con'); 
  con.onclick = function () {     
    setTimeout(function setTimeout1() {        
      con.textContent = 0;        
      Promise.resolve().then(function Promise1 () {
        console.log('Promise1')  
        })     
    }, 0)     
    setTimeout(function setTimeout2() {        
      con.textContent = 1;        
      Promise.resolve().then(function Promise2 () {
        console.log('Promise2') 
      })     
    }, 0) }; 
</script> 

當(dāng)兩個宏任務(wù)耗時不足一幀時,會發(fā)生渲染合并現(xiàn)象:

file

我們修改上訴代碼如下,延長第二個宏任務(wù)執(zhí)行時機:

<div id='con'>this is con</div> 
<script> 
  var con = document.getElementById('con'); 
  con.onclick = function () {     
    setTimeout(function setTimeout1() {        
      con.textContent = 0;        
      Promise.resolve().then(function Promise1 () {
        console.log('Promise1')  
        })     
    }, 0)     
    setTimeout(function setTimeout2() {        
      con.textContent = 1;        
      Promise.resolve().then(function Promise2 () {
        console.log('Promise2') 
      })     
    }, 17) }; 
</script> 

兩個宏任務(wù)執(zhí)行時間間隔 17ms ,按照代碼邏輯宏任務(wù)執(zhí)行完畢就進行渲染,并未發(fā)生渲染合并現(xiàn)象。

file

事件循環(huán)之任務(wù)拆分

作為提供數(shù)據(jù)中臺服務(wù)的公司,我們不可避免會涉及到一些復(fù)雜數(shù)據(jù)到計算。下面這個例子我們需要計算從1到10 000 000 000 數(shù)據(jù)加起來到合,計算完畢展示我們到彈框。通過 Performance 我們可以看見這個同步任務(wù)耗時快 4s,瀏覽器每幀需達到 60fps/s,也就是16.7ms 每幀,在這個計算結(jié)束之前其他任務(wù)均得不到執(zhí)行,導(dǎo)致后續(xù)渲染任務(wù)的延遲造成卡頓現(xiàn)象。

let i = 0; 
let start = Date.now(); 
function count() {   
  // long Task   
  for (let j = 0; j < 1e9; j++) {       
    i++;   
  }   
  alert("Done in " + (Date.now() - start) + 'ms'); 
} 
count(); 
file

我們希望這個 long Task 拆分成一個個小的任務(wù),解決長時間阻塞造成的卡頓現(xiàn)象。我們可以利用 setTimeout 拆分我們的任務(wù),修改代碼如下,這個計算確實被拆分成了一個個小任務(wù)。React Firber架構(gòu)中也使用了任務(wù)拆分這種思想將遞歸渲染 vdom 轉(zhuǎn)為了鏈表可中斷渲染 vdom,筆者對 Fiber了解并不多,這部分就不展開細說了。

let i = 0; 
let start = Date.now(); 
function count() {     
  // long Task     
  do{         
    i++;     
  }while(i % 1e6 != 0)     
    if(i == 1e9) {         
      alert("Done in " + (Date.now() - start) + 'ms');     
    } else {         
      setTimeout(count);     
    } } 
count(); 
file

setTimeout 確實將任務(wù)進行了拆分處理,但仍占用了主線程但資源,我們知道主線程保證了頁面的渲染、腳本交互、布局等操作,上訴這種單純的數(shù)據(jù)計算放在主線程處理是沒有意義的,我們可以將耗時計算放在 Web Worker 中處理。

事件循環(huán)優(yōu)化之Web Worker

什么是 Web Worker?

?? 當(dāng)在 HTML 頁面中執(zhí)行腳本時,頁面的狀態(tài)是不可響應(yīng)的,直到腳本已完成。web worker 是運行在后臺的 JavaScript,獨立于其他腳本,不會影響頁面的性能。您可以繼續(xù)做任何愿意做的事情:點擊、選取內(nèi)容等等,而此時 web worker 在后臺運行。

Web Worker 工作原理

file

Web Worker 改變了 JavaScript 單線程執(zhí)行這一本質(zhì)了嗎?

并沒有改變 JavaScript 是單線程執(zhí)行這一本質(zhì)。JavaScript 是一門沒有定義線程模型的原型,Web Worker 并不是 JavaScript 的一部分,它是瀏覽器提供的一種創(chuàng)建線程的方式,所以在使用 Web Worker 時不能操作 DOM,這也就意味著我們不能使用 Web Worker 進行 UI 更新這種操作,但如果把 Web Worker 理解成一個計算器,處理繁重的計算任務(wù),會讓我們的主線程執(zhí)行更加流暢。

// worker.js 
self.onmessage = (e=>{
    const { startNum } = e.data;
    let sum = startNum;
    (function count() {
        // long Task
        for (let j = 0; j < 1e9; j++) {
            sum++;
        }
    })();
    self.postMessage(sum);
})

// test.html 
let start = Date.now();
let worker = new Worker('worker.js');
worker.postMessage({startNum: 0});
worker.onmessage = (e) => {
  alert("Done in " + (Date.now() - start) + 'ms');
}
file

總結(jié)

筆者最初學(xué)習(xí)事件循環(huán)時只會判斷一段簡單代碼片段的輸出結(jié)果,查閱網(wǎng)上資料發(fā)現(xiàn)大部分資料也是這樣介紹事件循環(huán)的,這導(dǎo)致筆者思維長時間聚焦在一段腳本的輸出結(jié)果,接觸 Web Worker 時針對復(fù)雜計算開一個線程的必要性也持有懷疑。通過這段時間的學(xué)習(xí),讓我理解到事件循環(huán)的本質(zhì)是保證用戶交互、腳本、UI 渲染有序進行的基石,腳本長時間的執(zhí)行會導(dǎo)致后續(xù)任務(wù)阻塞,頁面呈現(xiàn)卡頓現(xiàn)象,這也是為什么 Web Worker 采用開一個線程進行復(fù)雜計算的原因。

最后

歡迎關(guān)注【袋鼠云數(shù)棧UED團隊】~
袋鼠云數(shù)棧 UED 團隊持續(xù)為廣大開發(fā)者分享技術(shù)成果,相繼參與開源了歡迎 star

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容