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