解讀JavaScript中的事件循環

解讀JavaScript中的事件循環

您可能已經知道 JavaScript 是一種單線程編程語言。這意味著 JavaScript 在 Web 瀏覽器或 Node.js 中的單個主線程上運行。在單個主線程上運行意味著一次僅運行一段 JavaScript 代碼。

JavaScript 中的事件循環在確定代碼如何在主線程上執行方面發揮著重要作用。事件循環負責一些事情,例如代碼的執行以及事件的收集和處理。它還處理任何排隊子任務的執行。

在本教程中,您將學習 JavaScript 中事件循環的基礎知識。

事件循環如何工作

為了理解事件循環的工作原理,您需要了解三個重要術語。

立即學習Java免費學習筆記(深入)”;

堆棧

調用堆棧只是跟蹤函數執行上下文的函數調用堆棧。該堆棧遵循后進先出 (LIFO) 原則,這意味著最近調用的函數將是第一個執行的函數。

隊列

隊列包含一系列由 JavaScript 執行的任務。該隊列中的任務可能會導致調用函數,然后將其放入堆棧中。僅當堆棧為空時才開始隊列的處理。隊列中的項目遵循先進先出 (FIFO) 原則。這意味著最舊的任務將首先完成。

堆基本上是存儲和分配對象的一大塊內存區域。它的主要目的是存儲堆棧中的函數可能使用的數據。

基本上,JavaScript 是單線程的,一次執行一個函數。這個單一函數被放置在堆棧上。該函數還可以包含其他嵌套函數,這些函數將放置在堆棧中的上方。堆棧遵循 LIFO 原則,因此最近調用的嵌套函數將首先執行。

API 請求或計時器等異步任務將添加到隊列以便稍后執行。 JavaScript 引擎在空閑時開始執行隊列中的任務。

考慮以下示例:

function helloWorld() {     console.log("Hello, World!"); }  function helloPerson(name) {     console.log(`Hello, ${name}!`); }  function helloTeam() {     console.log("Hello, Team!");     helloPerson("Monty"); }  function byeWorld() {     console.log("Bye, World!"); }  helloWorld(); helloTeam(); byeWorld();  /* Outputs:  Hello, World! Hello, Team! Hello, Monty! Bye, World!  */ 

讓我們看看如果運行上面的代碼,堆棧和隊列會是什么樣子。

調用 helloWorld() 函數并將其放入堆棧中。它記錄 Hello, World! 完成其執行,因此它被從堆棧中取出。接下來調用 helloTeam() 函數并將其放入堆棧中。在執行過程中,我們記錄 Hello, Team! 并調用 helloPerson()。 helloTeam() 的執行還沒有完成,所以它停留在堆棧上,而 helloPerson() 則放在它上面。

后進先出原則規定 helloPerson() 現在執行。這會將 Hello, Monty! 記錄到控制臺,從而完成其執行,并且 helloPerson() 將從堆棧中取出。之后 helloTeam() 函數就會出棧,我們最終到達 byeWorld()。它會記錄再見,世界!,然后從堆棧中消失。

隊列一直是空的。

現在,考慮上述代碼的細微變化:

function helloWorld() {     console.log("Hello, World!"); }  function helloPerson(name) {     console.log(`Hello, ${name}!`); }  function helloTeam() {     console.log("Hello, Team!");     setTimeout(() => {         helloPerson("Monty");     }, 0); }  function byeWorld() {     console.log("Bye, World!"); }  helloWorld(); helloTeam(); byeWorld();  /* Outputs:  Hello, World! Hello, Team! Bye, World! Hello, Monty!  */ 

我們在這里所做的唯一更改是使用 setTimeout()。但是,超時已設置為零。因此,我們期望 Hello, Monty!Bye, World! 之前輸出。如果您了解事件循環的工作原理,您就會明白為什么不會發生這種情況。

當helloTeam()入棧時,遇到setTimeout()方法。但是,setTimeout() 中對 helloPerson() 的調用會被放入隊列中,一旦沒有同步任務需要執行,就會被執行。

一旦對 byeWorld() 的調用完成,事件循環將檢查隊列中是否有任何掛起的任務,并找到對 helloPerson() 的調用。此時,它執行該函數并將 Hello, Monty! 記錄到控制臺。

這表明您提供給 setTimeout() 的超時持續時間并不是回調執行的保證時間。這是執行回調的最短時間。

保持我們的網頁響應

JavaScript 的一個有趣的特性是它會運行一個函數直到完成。這意味著只要函數在堆棧上,事件循環就無法處理隊列中的任何其他任務或執行其他函數。

這可能會導致網頁“掛起”,因為它無法執行其他操作,例如處理用戶輸入或進行與 DOM 相關的更改。考慮以下示例,我們在其中查找給定范圍內的素數數量:

function isPrime(num) {   if (num  <p>在我們的 listPrimesInRange() 函數中,我們迭代從 start 到 end 的數字。對于每個數字,我們調用 isPrime() 函數來查看它是否是素數。 isPrime() 函數本身有一個 for 循環,該循環從 2 到 Math.sqrt(num) 來確定數字是否為素數。</p> <p>查找給定范圍內的所有素數可能需要一段時間,具體取決于您使用的值。當瀏覽器進行此計算時,它無法執行任何其他操作。這是因為 listPrimesInRange() 函數將保留在堆棧中,瀏覽器將無法執行隊列中的任何其他任務。</p> <p>現在,看一下以下函數:</p> <pre class="brush:javascript;toolbal:false;">function listPrimesInRangeResponsively(start) {   let next = start + 100,000;    if (next &gt; end) {     next = end;   }    for (let num = start; num  {           listPrimesInRangeResponsively(next + 1);         });       }     }      if (num == end) {       percentage = ((num - begin) * 100) / (end - begin);       percentage = Math.floor(percentage);        progress.innerText = `Progress ${percentage}%`;        heading.innerText = `${primeNumbers.length - 1} Primes Found!`;        console.log(primeNumbers);        return primeNumbers;     }   } } 

這一次,我們的函數僅在批量處理范圍時嘗試查找素數。它通過遍歷所有數字但一次僅處理其中的 100,000 個來實現這一點。之后,它使用 setTimeout() 觸發對同一函數的下一次調用。

當 setTimeout() 被調用而沒有指定延遲時,它會立即將回調函數添加到事件隊列中。

下一個調用將被放入隊列中,暫時清空堆棧以處理任何其他任務。之后,JavaScript 引擎開始在下一批 100,000 個數字中查找素數。

嘗試單擊此頁面上的計算(卡住)按鈕,您可能會收到一條消息,指出該網頁正在減慢您的瀏覽器速度,并建議您停止該腳本。 p>

另一方面,單擊計算(響應式)按鈕仍將使網頁保持響應式。

最終想法

在本教程中,我們了解了 JavaScript 中的事件循環以及它如何有效地執行同步和異步代碼。事件循環使用隊列來跟蹤它必須執行的任務。

由于 JavaScript 不斷執行函數直至完成,因此進行大量計算有時會“掛起”瀏覽器窗口。根據我們對事件循環的理解,我們可以重寫我們的函數,以便它們批量進行計算。這允許瀏覽器保持窗口對用戶的響應。它還使我們能夠定期向用戶更新我們在計算中取得的進展。

? 版權聲明
THE END
喜歡就支持一下吧
點贊9 分享