JS面試高頻單線程與事件循環(huán)深入解析
一、背景:為什么JS是單線程?
在最開始設(shè)計中,JS的主要用途是處理瀏覽器中的用戶界面事件。由于JS交互直接進(jìn)行DOM操作,如果允許多線程對DOM進(jìn)行并行操作,可能會導(dǎo)致競態(tài)條件,例如一個線程正在讀取節(jié)點,而另一個線程正在修改它。這將導(dǎo)致程序的不可預(yù)測性,因此,JS被設(shè)計為單線程語言,以避免這種復(fù)雜性。
二、事件循環(huán)
事件循環(huán)的核心思想是:JS引擎首先執(zhí)行當(dāng)前的同步任務(wù),然后檢查任務(wù)隊列(Task Queue)中是否有待處理的異步任務(wù)。如果有,它會按照順序?qū)⑦@些異步任務(wù)添加到執(zhí)行隊列,并在當(dāng)前任務(wù)執(zhí)行完畢后依次執(zhí)行它們。在這個過程中,宏任務(wù)和微任務(wù)是兩種不同類型的異步任務(wù),它們在事件循環(huán)中的處理方式有所不同。
2.1 宏任務(wù)(MacroTask)
宏任務(wù)是指那些需要在下一個事件循環(huán)周期執(zhí)行的任務(wù)。常見的宏任務(wù)包括:
setTimeout
setInterval
setImmediate
(Node.js 獨有)- I/O 操作(Node.js 獨有)
- UI 渲染(瀏覽器獨有)
當(dāng)事件循環(huán)執(zhí)行到一個宏任務(wù)時,它會將該任務(wù)添加到宏任務(wù)隊列中。在當(dāng)前事件循環(huán)周期結(jié)束時,JS引擎會檢查宏任務(wù)隊列,并將隊列中的任務(wù)依次執(zhí)行。
2.2 微任務(wù)(MicroTask)
微任務(wù)是指那些在當(dāng)前事件循環(huán)周期內(nèi)執(zhí)行的任務(wù)。常見的微任務(wù)包括:
Promise.then
和Promise.catch
async/await
(實際上是基于 Promise 的語法糖)process.nextTick
(Node.js 獨有)MutationObserver
(瀏覽器獨有)
當(dāng)事件循環(huán)執(zhí)行到一個微任務(wù)時,它會將該任務(wù)添加到微任務(wù)隊列中。與宏任務(wù)不同,微任務(wù)會在當(dāng)前事件循環(huán)周期內(nèi)立即執(zhí)行,而不是等待下一個事件循環(huán)周期。
2.3 事件循環(huán)處理宏任務(wù)和微任務(wù)的順序
- 從宏任務(wù)隊列中取出一個任務(wù)并執(zhí)行。
- 檢查微任務(wù)隊列,如果有任務(wù),則依次執(zhí)行所有微任務(wù)。
- 檢查宏任務(wù)隊列,如果有任務(wù),則返回步驟1,否則等待新任務(wù)。
這意味著,在一個事件循環(huán)周期中,微任務(wù)會在宏任務(wù)之間執(zhí)行。換句話說,當(dāng)一個宏任務(wù)執(zhí)行完畢后,JS引擎會檢查微任務(wù)隊列,并在執(zhí)行下一個宏任務(wù)之前執(zhí)行所有的微任務(wù)。
下面是一個簡單的示例,展示了宏任務(wù)和微任務(wù)在事件循環(huán)中的執(zhí)行順序:
console.log('Start'); // 同步任務(wù) setTimeout(() => { console.log('setTimeout'); // 宏任務(wù) }, 0); Promise.resolve().then(() => { console.log('Promise'); // 微任務(wù) }); console.log('End'); // 同步任務(wù)
輸出順序為:
Start
End
Promise
setTimeout
這是因為在執(zhí)行到 setTimeout
時,它被添加到宏任務(wù)隊列中。而在執(zhí)行到 Promise
時,它被添加到微任務(wù)隊列中。在當(dāng)前事件循環(huán)周期結(jié)束之前,JS引擎會先執(zhí)行微任務(wù)隊列中的所有任務(wù),然后再執(zhí)行宏任務(wù)隊列中的任務(wù)。
事件循環(huán)是JavaScript運行時環(huán)境的核心組件,負(fù)責(zé)處理宏任務(wù)和微任務(wù)。了解宏任務(wù)和微任務(wù)在事件循環(huán)中的執(zhí)行順序,有助于我們更好地理解和編寫異步代碼。
三、 異步編程
- 回調(diào)函數(shù):最基本的異步編程模型,將一個函數(shù)作為參數(shù)傳遞給另一個函數(shù),當(dāng)異步操作完成時,回調(diào)函數(shù)被執(zhí)行。
function downloadFile(url, callback) { // 模擬異步操作 setTimeout(() => { console.log(`Downloaded file from ${url}`); callback(); }, 2000); } downloadFile('https://example.com/file.txt', function() { console.log('File download complete'); });
- Promise:Promise是一種更高級的異步編程模型,它表示一個異步操作的最終結(jié)果。Promise有三種狀態(tài):pending(進(jìn)行中)、fulfilled(已成功)和rejected(已失?。?。
function downloadFile(url) { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`Downloaded file from ${url}`); resolve(); }, 2000); }); } downloadFile('https://example.com/file.txt') .then(() => { console.log('File download complete'); });
- async/await:async/await是基于Promise的一種更簡潔的異步編程模型。通過使用async關(guān)鍵字聲明一個函數(shù)為異步函數(shù),然后在函數(shù)內(nèi)部使用await關(guān)鍵字等待Promise的結(jié)果。
async function downloadFile(url) { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`Downloaded file from ${url}`); resolve(); }, 2000); }); } (async () => { await downloadFile('https://example.com/file.txt'); console.log('File download complete'); })();
三、單線程的優(yōu)缺點
3.1 優(yōu)點
- 避免了多線程下的復(fù)雜性,如死鎖。
- 簡化了異步操作,使得異步編程更易于構(gòu)建和理解。
3.2 缺點
- 長時間運行的任務(wù)可能會阻塞線程,影響用戶體驗。
- 無法充分利用多核CPU的計算能力。
四、實現(xiàn)多線程的方法
盡管JS是單線程,但我們可以通過Web Workers在瀏覽器中創(chuàng)建多個線程。Web Workers運行在后臺線程中,不影響主線程,它們之間通過postMessage來進(jìn)行通信。
4.1 Web Workers
這是一個簡單的Web Worker示例,演示了多個進(jìn)程之間的通信:
// main.js const worker = new Worker('worker.js'); worker.postMessage('Hello, Worker!'); worker.onmessage = function(event) { console.log('Message from worker:', event.data); }; // worker.js self.onmessage = function(event) { console.log('Message from main thread:', event.data); self.postMessage('Hello, Main Thread!'); };
4.2 SharedArrayBuffer與Atomics
為了實現(xiàn)更高級的多線程編程,JS引入了SharedArrayBuffer和Atomics對象。SharedArrayBuffer允許多個Web Workers共享同一塊內(nèi)存,而Atomics對象提供了一組原子操作,確保在多線程環(huán)境下對共享內(nèi)存的操作是安全的。
以下是一個使用SharedArrayBuffer和Atomics的示例:
// main.js const worker = new Worker('worker.js'); const sharedBuffer = new SharedArrayBuffer(4); const sharedArray = new Int32Array(sharedBuffer); worker.postMessage(sharedBuffer); Atomics.store(sharedArray, 0, 1); console.log('Main thread set value:', sharedArray[0]); worker.onmessage = function(event) { console.log('Message from worker:', event.data); }; // worker.js self.onmessage = function(event) { const sharedBuffer = event.data; const sharedArray = new Int32Array(sharedBuffer); console.log('Worker thread initial value:', sharedArray[0]); Atomics.add(sharedArray, 0, 1); console.log('Worker thread updated value:', sharedArray[0]); self.postMessage('SharedArrayBuffer updated'); };
4.3 使用Web Workers的注意事項
- Web Workers無法訪問主線程的全局變量和函數(shù)。
- Web Workers無法直接操作DOM。
- 通信開銷:Web Workers之間的通信需要通過postMessage和onmessage事件進(jìn)行,這會帶來一定的性能開銷。
總結(jié)
JS的單線程特性使得編程模型簡單易懂,但也帶來了一些限制。通過使用事件循環(huán)、異步編程模型和Web Workers,我們可以在很大程度上克服這些限制,進(jìn)而實現(xiàn)高性能的Web應(yīng)用。
以上就是JS面試高頻單線程與事件循環(huán)深入解析的詳細(xì)內(nèi)容,更多關(guān)于JS單線程事件循環(huán)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Javascript在IE和FireFox中的不同表現(xiàn)簡析
本文將詳細(xì)介紹Javascript在IE和FireFox中的不同表現(xiàn),本人整理了一下,需要的朋友可以參考下2012-12-12

JavaScript檢測瀏覽器cookie是否已經(jīng)啟動的方法