相親交友原始碼中的事件循壞,你瞭解多少?

雲豹科技程式設計師發表於2021-11-08

先來了解一下三個重要的概念

主執行緒

相親交友原始碼中所有的同步任務都是在主執行緒裡執行的,非同步任務可能會在macrotask或者microtask裡面

  • 同步任務: 指的是在相親交友原始碼主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
  • 非同步任務: 指的是不進入相親交友原始碼主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

微任務(micro task)

  • promise
  • async
  • await
  • process.nextTick(node)
  • mutationObserver(html5新特性)

巨集任務(macro task)

  • script(整體程式碼)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

大致流程

簡單的說,事件迴圈(eventLoop)是單執行緒的JavaScript在處理非同步事件時進行的一種迴圈過程,具體來講,對於相親交友原始碼的非同步事件它會先加入到事件佇列中掛起,等主執行緒空閒時會去執行事件佇列中的事件。
主執行緒任務——>微任務——>巨集任務 如果巨集任務裡還有微任就繼續執行巨集任務裡的微任務,如果巨集任務中的微任務中還有巨集任務就在依次進行
主執行緒任務——>微任務——>巨集任務——>巨集任務裡的微任務——>巨集任務裡的微任務中的巨集任務——>直到任務全部完成 我的理解是在同級下,微任務要優先於巨集任務執行

在相親交友原始碼同一輪任務佇列中,同一個微任務產生的微任務會放在這一輪微任務的後面,產生的巨集任務會放在這一輪的巨集任務後面
在相親交友原始碼同一輪任務佇列中,同一個巨集任務產生的微任務會馬上執行,產生的巨集任務會放在這一輪的巨集任務後面

它不停檢查 Call Stack 中是否有任務(也叫棧幀)需要執行,如果沒有,就檢查 Event Queue,從中彈出一個任務,放入 Call Stack 中,如此往復迴圈。

在這裡插入圖片描述

  • 同步和非同步任務分別進入不同的執行"場所",同步的進入相親交友原始碼主執行緒,非同步的進入Event Table並註冊函式。

  • 當指定的事情完成時,Event Table會將這個函式移入Event Queue。

  • 相親交友原始碼主執行緒內的任務執行完畢為空,會去Event Queue讀取對應的函式,進入主執行緒執行。

  • 上述過程會不斷重複,也就是常說的Event Loop(事件迴圈)

    事件迴圈流程函式版

堆疊和佇列簡要說明圖

在這裡插入圖片描述

為什麼先微後巨集

微任務會在執行任何其他事件處理,或渲染,或執行任何其他巨集任務之前完成。
這很重要,因為它確保了微任務之間的應用程式環境基本相同(沒有滑鼠座標更改,沒有新的網路資料等)。
如果我們想要非同步執行(在當前程式碼之後)一個函式,但是要在更改被渲染或新事件被處理之前執行,那麼我們可以使用 queueMicrotask 來對其進行安排(schedule)

其他概念名詞

堆(heap)

儲存的地址

物件被分配在相親交友原始碼堆中,堆是一個用來表示一大塊(通常是非結構化的)記憶體區域的計算機術語。

棧(stack)

後進先出 (lifo) last in first out (坐電梯?第一個進電梯的人最後一個出來,最後一個進電梯的人第一個出來!)
函式呼叫形成了一個由若干幀組成的棧。

function foo(b) {
  let a = 10;
  return a + b + 11;}function bar(x) {
  let y = 3;
  return foo(x * y);}console.log(bar(7)); // 返回 42```

當呼叫 bar 時,第一個幀被建立並壓入棧中,幀中包含了 bar 的引數和區域性變數。 當 bar 呼叫 foo 時,第二個幀被建立並被壓入棧中,放在第一個幀之上,幀中包含 foo 的引數和區域性變數。當 foo 執行完畢然後返回時,第二個幀就被彈出棧(剩下 bar 函式的呼叫幀 )。當 bar 也執行完畢然後返回時,第一個幀也被彈出,棧就被清空了。

佇列(queue)

先進先出 (fifo) first in first out
當 Event Table 中的事件被觸發,事件對應的 回撥函式 就會被 push 進這個 Event Queue,然後等待被執行
一個 JavaScript 執行時包含了一個待處理訊息的訊息佇列。每一個訊息都關聯著一個用以處理這個訊息的回撥函式。
在 事件迴圈 期間的某個時刻,執行時會從最先進入佇列的訊息開始處理佇列中的訊息。被處理的訊息會被移出佇列,並作為輸入引數來呼叫與之關聯的函式。正如前面所提到的,呼叫一個函式總是會為其創造一個新的棧幀。
函式的處理會一直進行到執行棧再次為空為止;然後相親交友原始碼的事件迴圈將會處理佇列中的下一個訊息(如果還有的話)。

事件表格(event table)

Event Table 可以理解成一張事件->回撥函式 對應表
呼叫web apis來執行函式然後回撥到事件佇列中
它就是用來儲存 Js 中的非同步事件 (request, setTimeout, IO等) 及其對應的回撥函式的列表

Web APIs

瀏覽器提供了多種非同步的Web API,如DOM,times(計時器),AJAX等。
當我們呼叫一個 Web API 時,如 setTimeout,setTimeout函式會被 push 呼叫棧頂然後執行,但是 setTimeout 的回撥函式不會立即被 push 到呼叫棧頂,而是起一個計時器任務。當這個計時器結束時,該回撥函式會被塞到任務佇列(CallBack Queue)中。這個佇列中的回撥函式的呼叫就是由事件迴圈機制來控制的。
理解: 相親交友原始碼呼叫任務時,並不會馬上進入任務佇列,而是先呼叫web提供的api,等待執行的結果才放進去任務佇列

Web Workers

對於不應該阻塞事件迴圈的耗時長的繁重計算任務,我們可以使用 Web Workers。
這是在另一個並行執行緒中執行相親交友原始碼的方式。
Web Workers 可以與主執行緒交換訊息,但是它們具有自己的變數和事件迴圈。
Web Workers 沒有訪問 DOM 的許可權,因此,它們對於同時使用多個 CPU 核心的計算非常有用。

程式和執行緒

舉一個 ? (測試學到了沒有)

console.log('script start')async function async1() {
    await async2() 
    console.log('async1 end')}async function async2() {
    console.log('async2 end')}async1()setTimeout(function() {
    console.log('setTimeout')}, 0)new Promise(resolve => {
    console.log('Promise')resolve()}).then(function() {
    console.log('promise1')}).then(function() {
    console.log('promise2')})console.log('script end')// 新版輸出(新版的chrome瀏覽器優化了,await變得更快了,輸出為)// script start => async2 end => Promise => script end => async1 end => promise1 => promise2  => setTimeout// 注意一個點await async2() 執行完後面的任務才會註冊到微任務中
 
 // 舊版輸出如下,但是請繼續看完本文下面的注意那裡,新版有改動// script start => async2 end => Promise => script end => promise1 => promise2 => **async1 end** => setTimeout

但是這種做法其實是違法了規範的,當然規範也是可以更改的。

如何新增巨集任務和微任務

安排(schedule)一個新的 巨集任務:

  • 使用零延遲的 setTimeout(f)。

它可被用於相親交友原始碼將繁重的計算任務拆分成多個部分,以使瀏覽器能夠對使用者事件作出反應,並在任務的各部分之間顯示任務進度。
此外,也被用於在事件處理程式中,將一個行為(action)安排(schedule)在事件被完全處理(冒泡完成)後。
安排一個新的 微任務:

  • 使用 queueMicrotask(f)。
  • promise 處理程式也會通過微任務佇列。

在微任務之間沒有 UI 或網路事件的處理:它們一個立即接一個地執行。
所以,我們可以使用 queueMicrotask 來在保持環境狀態一致的情況下,非同步地執行一個函式。

分析setTimeout

setTimeout(fn,0)
的含義是,指定相親交友原始碼某個任務在主執行緒最早可得的空閒時間執行,意思就是不用再等多少秒了,只要主執行緒執行棧內的同步任務全部執行完成,棧為空就馬上執行

setTimeout(() => {
  task()},3000)sleep(10000000)

乍一看其實差不多嘛,但我們把這段程式碼在chrome執行一下,卻發現控制檯執行task()需要的時間遠遠超過3秒,說好的延時三秒,為啥現在需要這麼長時間啊?
這時候我們需要重新理解setTimeout的定義。我們先說上述程式碼是怎麼執行的:

  • task()進入Event Table並註冊,計時開始。
  • 執行sleep函式,很慢,非常慢,計時仍在繼續。
  • 3秒到了,計時事件timeout完成,task()進入Event Queue,但是sleep也太慢了吧,還沒執行完,只好等著。
  • sleep終於執行完了,task()終於從Event Queue進入了主執行緒執行。

事件迴圈的其他應用

1 拆分CPU過載任務

假設我們有一個 CPU 過載任務。
例如,相親交友原始碼語法高亮(用來給本頁面中的示例程式碼著色)是相當耗費 CPU 資源的任務。為了高亮顯示程式碼,它執行分析,建立很多著了色的元素,然後將它們新增到文件中 —— 對於文字量大的文件來說,需要耗費很長時間。
當引擎忙於語法高亮時,它就無法處理其他 DOM 相關的工作,例如處理相親交友原始碼使用者事件等。它甚至可能會導致瀏覽器“中斷(hiccup)”甚至“掛起(hang)”一段時間,這是不可接受的。
我們可以通過將大任務拆分成多個小任務來避免這個問題。高亮顯示前 100 行,然後使用 setTimeout(延時引數為 0)來安排(schedule)後 100 行的高亮顯示,依此類推。
為了演示這種方法,簡單起見,讓我們寫一個從 1 數到 1000000000 的函式,而不寫文字高亮。
如果你執行下面這段程式碼,你會看到引擎會“掛起”一段時間。對於服務端 JS 來說這顯而易見,並且如果你在瀏覽器中執行它,嘗試點選頁面上其他按鈕時,你會發現在計數結束之前不會處理其他事件。

let i = 0;let start = Date.now();function count() {
  // 做一個繁重的任務
  for (let j = 0; j < 1e9; j++) {
    i++;
  }
  alert("Done in " + (Date.now() - start) + 'ms');}count();

瀏覽器甚至可能會顯示一個“指令碼執行時間過長”的警告。
讓我們使用巢狀的 setTimeout 呼叫來拆分這個任務:

let i = 0;let start = Date.now();function count() {
  // 做繁重的任務的一部分 (*)
  do {
    i++;
  } while (i % 1e6 != 0);
  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count); // 安排(schedule)新的呼叫 (**)
  }}count();

現在,瀏覽器介面在“計數”過程中可以正常使用。
單次執行 count 會完成工作 (*) 的一部分,然後根據需要重新安排(schedule)自身的執行 (**):

  1. 首先執行計數:i=1…1000000。
  2. 然後執行計數:i=1000001…2000000。
  3. ……以此類推。

現在,如果在引擎忙於執行第一部分時出現了一個新的副任務(例如 onclick 事件),則該任務會被排入佇列,然後在第一部分執行結束時,並在下一部分開始執行前,會執行該副任務。週期性地在兩次 count 執行期間返回事件迴圈,這為 JavaScript 引擎提供了足夠的“空氣”來執行其他操作,以響應相親交友原始碼其他的使用者行為。
值得注意的是這兩種變體 —— 是否使用了 setTimeout 對任務進行拆分 —— 在執行速度上是相當的。在執行計數的總耗時上沒有多少差異。
為了使兩者耗時更接近,讓我們來做一個改進。
我們將要把排程(scheduling)移動到 count() 的開頭:

let i = 0;let start = Date.now();function count() {
  // 將排程(scheduling)移動到開頭
  if (i < 1e9 - 1e6) {
    setTimeout(count); // 安排(schedule)新的呼叫
  }
  do {
    i++;
  } while (i % 1e6 != 0);
  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  }}count();

現在,當我們開始呼叫 count() 時,會看到我們需要對 count() 進行更多呼叫,我們就會在工作前立即安排(schedule)它。
如果你執行它,你很容易注意到它花費的時間明顯減少了。
為什麼?
這很簡單:你應該還記得,多個巢狀的 setTimeout 呼叫在瀏覽器中的最小延遲為 4ms。即使我們設定了 0,但還是 4ms(或者更久一些)。所以我們安排(schedule)得越早,執行速度也就越快。
最後,我們將相親交友原始碼中一個繁重的任務拆分成了幾部分,現在它不會阻塞使用者介面了。而且其總耗時並不會長很多。

2 進度指示

對瀏覽器指令碼中的過載型任務進行拆分的另一個好處是,我們可以顯示進度指示。
正如前面所提到的,僅在當前執行的任務完成後,才會對 DOM 中的更改進行繪製,無論這個任務執行花費了多長時間。
從一方面講,這非常好,因為我們的函式可能會建立很多元素,將它們一個接一個地插入到文件中,並更改其樣式 —— 訪問者不會看到任何未完成的“中間態”內容。很重要,對吧?
這是一個示例,對 i 的更改在該函式完成前不會顯示出來,所以我們將只會看到最後的值:

<div id="progress"></div><script> function count() {
  for (let i = 0; i < 1e6; i++) {
    i++;
    progress.innerHTML = i;
  }
 }
 
 count(); 
 </script>

……但是我們也可能想在任務執行期間展示一些東西,例如進度條。
如果我們使用 setTimeout 將繁重的任務拆分成幾部分,那麼變化就會被在它們之間繪製出來。
這看起來更好看:

<div id="progress"></div><script> 
  let i = 0;
  function count() {
    // 做繁重的任務的一部分 (*)
    do {
      i++;
      progress.innerHTML = i;
    } 
    while (i % 1e3 != 0);
    if (i < 1e7) {
      setTimeout(count);
    }
  }
  count(); 
  </script>

現在 div 顯示了 i 的值的增長,這就是進度條的一種

node 和 瀏覽器 eventLoop的主要區別

兩者最主要的區別在於瀏覽器中的微任務是在每個相應的巨集任務中執行的,而nodejs中的微任務是在不同階段之間執行的。

總結

  • 微任務佇列優先於巨集任務佇列執行;
  • 相親交友原始碼微任務佇列上建立的巨集任務會被後新增到當前巨集任務佇列的尾端;
  • 微任務佇列中建立的微任務會被新增到微任務佇列的尾端;
  • 只要相親交友原始碼微任務佇列中還有任務,巨集任務佇列就只會等待微任務佇列執行完畢後再執行;
  • 只有執行完await語句,才把await語句後面的全部程式碼加入到微任務行列;
  • 在遇到await promise時,必須等await promise 函式執行完畢才能對await語句後面的全部程式碼加入到微任務中;

o 在等待 await Promise.then微任務時:

  •  執行其他同步程式碼;
  •  等到同步程式碼執行完,開始執行 await promise.then 微任務;
  •  await promise.then微任務完成後,把await語句後面的全部程式碼加入到微任務行列;

本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理
原文連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2841138/,如需轉載,請註明出處,否則將追究法律責任。

相關文章