為什麼會突然想到寫這麼一個大雜燴的博文呢,必須要從筆者幾年前的一次面試說起。當時的我年輕氣盛,在簡歷上放了自己的博客地址,而面試官應該是翻了我的博客,好幾道面試題都是圍繞著我的博文來提問。其中一個問題,直接使得空氣靜止了五分鐘,也是自從那次面試,我告訴自己,工作實戰中總結的經驗,一定要知其然知其所以... ...
為什麼會突然想到寫這麼一個大雜燴的博文呢,必須要從筆者幾年前的一次面試說起
當時的我年輕氣盛,在簡歷上放了自己的博客地址,而面試官應該是翻了我的博客,好幾道面試題都是圍繞著我的博文來提問
其中一個問題,直接使得空氣靜止了五分鐘,然後面試官結束了這次面試,那就是:如何手寫一個簡易的Promise對象?
在這裡,我也先挖個坑,給你們五分鐘思考並自己回答一下這個問題~ (答案隱藏在文章中自行查看~)
也是自從那次面試,我告訴自己,工作實戰中總結的經驗,一定要知其然知其所以然,才可以真正用好這些核心知識點,不積跬步,無以至千里
說了這麼多的廢話,我們進入今天的博文正題~
目錄
什麼是事件迴圈(Event Loop)
事件迴圈是JavaScript運行時環境的核心機制,用於協調事件、用戶交互、腳本、渲染、網路等。
由於JavaScript是單線程的,事件迴圈使得它能夠執行非阻塞操作,即使在處理IO等長時間運行的任務時也能保持響應性。
事件迴圈的執行順序
在JavaScript的執行模型中,事件迴圈按照以下順序處理任務:
- 執行全局腳本代碼,這些同步代碼直接運行。
- 當執行棧為空時,事件迴圈會查看微任務隊列。如果隊列中有微任務,就一直執行微任務直到隊列清空。
- 執行一個巨集任務(如由
setTimeout()
或setInterval()
設置的回調)。 - 巨集任務執行完畢後,再次執行所有微任務。
- 如果有必要,進行UI渲染。
- 開始下一輪事件迴圈,處理下一個巨集任務。
通過這種機制,JavaScript可以在單線程中有效地處理非同步事件,同時保持代碼執行的順序和預期效果。
理解這些概念將幫助你更好地設計和調試JavaScript中的非同步代碼。
什麼是巨集任務(MacroTasks)和 微任務(MicroTasks)
巨集任務
巨集任務是 JavaScript 事件迴圈中的一個較大的任務單元,每個巨集任務在執行時會開啟一個新的事件迴圈
一個巨集任務的完成通常會涉及到一個較為完整的工作流程,例如整個腳本的執行、事件(如用戶交互事件)、定時器事件(setTimeout、setInterval)以及瀏覽器的 UI 渲染等
每個巨集任務在執行完畢後,會從任務隊列中清除
常見巨集任務
setTimeout()
:用於設置定時器,在指定的時間間隔後執行任務setInterval()
:用於設置定時器,在指定的時間間隔迴圈執行任務setImmediate()
:類似setTimeout(fn, 0)
(僅在Node.js中)- IO操作:例如文件讀寫、網路請求等
- UI渲染:瀏覽器需要重新渲染頁面時觸發的任務
requestAnimationFrame
:動畫渲染函數
拓展提問:點擊和鍵盤事件是巨集任務嗎?
在 JavaScript 中,事件(如點擊和鍵盤事件) 通常被處理為任務
但它們不是巨集任務(macro-tasks)也不是微任務(micro-tasks),而是作為任務隊列中的任務來處理
這些任務在巨集任務和微任務之外,有自己的特殊隊列,通常稱為 任務隊列(task queue)
事件(如點擊和鍵盤事件) 通常被放入任務隊列,並且它們被視為任務的一種。當
事件迴圈執行時,它會首先檢查巨集任務隊列,執行完當前巨集任務後,再執行所有的微任務。
在微任務執行完畢後,瀏覽器可能會進行渲染操作(如果需要),然後事件迴圈會繼續到下一個巨集任務。
因此,可以說點擊和鍵盤事件是作為任務處理的,而不特定分類為巨集任務或微任務。
這種機制確保了 JavaScript 可以在單線程環境中高效地處理非同步事件和操作,同時保持代碼執行的順序性和可預測性。
微任務
微任務是在當前巨集任務執行完畢後立即執行的任務,事件迴圈會在每個巨集任務之後執行所有隊列中的微任務
它們的執行時機是在下一個巨集任務開始之前,當前巨集任務的後續階段,微任務的執行時間早於巨集任務
微任務通常用於處理非同步操作的結果,確保儘可能快地響應
常見微任務
Promise.then/catch/finally
- Promise回調:當Promise狀態改變時,會執行相應的回調函數
async
/await
:使用async函數和await關鍵字進行非同步操作時,await後面的代碼會作為微任務執行process.nextTick
:在 Node.js 的事件迴圈的當前階段完成後、下一個事件迴圈階段開始之前,安排一個回調函數儘快執行 (僅在Node.js中)MutaionObserver()
:瀏覽器中用於觀察DOM樹的變化,監聽DOM變化,當DOM發生變化時觸發微任務
巨集任務和微任務的區別
任務特征
- 巨集任務 有明確的非同步任務需要執行和回調;需要其他非同步線程支持
- 微任務 沒有明確的非同步任務需要執行,只有回調,不需要其他非同步線程支持
存放位置
- 巨集任務 中的事件放在
callback queue
中,由事件觸發線程維護 - 微任務 的事件放在微任務隊列中,由js引擎線程維護
執行順序
- 事件迴圈的過程中,執行棧在同步代碼執行完成後,優先檢查 微任務 隊列是否有任務需要執行,如果沒有,再去 巨集任務 隊列檢查是否有任務執行,如此往複
- 微任務 一般在當前迴圈就會優先執行,而 巨集任務 會等到下一次迴圈
- 因此,微任務 一般比 巨集任務 先執行
隊列數量
- 微任務 隊列只有一個
- 巨集任務 隊列可能有多個
什麼是 Promise
對象
在 JavaScript 中,Promise
對象是非同步編程的一種重要機制,它代表了一個尚未完成但預期將來會完成的操作的最終結果。
Promise
提供了一種處理非同步操作的方法,使得非同步代碼易於編寫和理解。
Promise
的基本概念
Promise
對象有三種狀態:
- Pending(等待中):初始狀態,既不是成功,也不是失敗。
- Fulfilled(已完成):意味著操作成功完成。
- Rejected(已拒絕):意味著操作失敗或出現錯誤。
如何創建 Promise
對象
Promise
對象是通過 new Promise
構造函數創建的,它接收一個執行器函數作為參數。
這個執行器函數本身接受兩個參數:resolve
和 reject
,這兩個參數也是函數。
當非同步操作成功時,調用 resolve
函數;當操作失敗時,調用 reject
函數。
const myPromise = new Promise((resolve, reject) => {
// 非同步操作
const condition = true; // 假設這是某種條件判斷
if (condition) {
resolve('Operation successful');
} else {
reject('Error occurred');
}
});
如何使用 Promise
對象
一旦 Promise
被解析(resolved)或拒絕(rejected),它就不能更改狀態。
你可以使用 .then()
方法來處理已完成的 Promise
,並使用 .catch()
方法來處理被拒絕的 Promise
。
還有 .finally()
方法,它在 Promise
完成後被調用,無論其結果如何。
myPromise
.then(result => {
console.log(result); // 處理結果
})
.catch(error => {
console.error(error); // 處理錯誤
})
.finally(() => {
console.log('Operation completed'); // 最終都會執行
});
Promise
的優勢
- 鏈式調用:
Promise
允許你通過.then()
方法鏈式調用多個非同步操作,每個操作依次執行。 - 錯誤處理:通過
.catch()
方法,可以集中處理多個非同步操作中的錯誤。 - 並行處理:
Promise.all()
方法允許並行執行多個非同步操作,並等待所有操作完成。
Promise
在工作中的應用場景
Promise
在處理如網路請求、文件操作等非同步操作時非常有用,它使得代碼更加清晰,減少了回調地獄(callback hell)的問題。
通過 Promise
,開發者可以寫出更加優雅和可維護的非同步代碼。
如何快速入門上手JavaScript中的 Promise
?
拓展資料 ———— 快速入門上手JavaScript中的Promise
解答文章開頭的問題:如何手寫一個簡易的 Promise
對象?
function SimplePromise(executor) {
let onResolve, onReject;
let fulfilled = false;
let rejected = false;
let called = false; // 防止resolve和reject被多次調用
let value;
let reason;
// resolve函數
function resolve(val) {
if (!called) {
value = val;
fulfilled = true;
called = true;
if (onResolve) {
onResolve(val);
}
}
}
// reject函數
function reject(err) {
if (!called) {
reason = err;
rejected = true;
called = true;
if (onReject) {
onReject(err);
}
}
}
// then方法
this.then = function(callback) {
onResolve = callback;
if (fulfilled) {
onResolve(value);
}
return this; // 支持鏈式調用
};
// catch方法
this.catch = function(callback) {
onReject = callback;
if (rejected) {
onReject(reason);
}
return this; // 支持鏈式調用
};
// 立即執行傳入的executor函數
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
// 使用示例
let promise = new SimplePromise((resolve, reject) => {
setTimeout(() => {
resolve("Success!");
// reject("Error!"); // 也可以測試reject情況
}, 1000);
});
promise.then(result => {
console.log(result); // 輸出 "Success!"
}).catch(error => {
console.log(error);
});
什麼是定時器函數
JavaScript 中的定時器函數允許你在一定時間後或者以指定的時間間隔重覆執行代碼。
這些功能主要通過兩個全局函數實現:setTimeout()
和 setInterval()
。
這些函數是非同步的,意味著它們不會阻塞代碼的執行,而是在指定的延時後將任務加入到 JavaScript 的事件隊列中,等待當前執行棧清空後再執行。
setTimeout()
setTimeout()
函數用於在指定的毫秒數後執行一個函數或指定的代碼。
它不會阻止後續代碼的執行,而是在背後計時,一旦時間到達,就將回調函數加入到事件隊列中,等待執行。
語法
let timeoutID = setTimeout(function[, delay, arg1, arg2, ...]);
function
:要執行的函數。delay
:延遲的時間,以毫秒為單位。如果省略,或者為 0,瀏覽器通常會有最小延遲時間(在HTML5標準中定義為4ms)。arg1, arg2, ...
:傳遞給函數的額外參數。
使用示例
console.log("Hello");
setTimeout(() => {
console.log("World!");
}, 1000);
這個例子會先列印 "Hello",然後大約1秒後列印 "World!"
setInterval()
setInterval()
函數用於重覆調用一個函數或執行代碼片段,每隔指定的周期時間(以毫秒為單位)。
它也是非阻塞的,每次間隔時間到達後,就會嘗試執行指定的代碼。
語法
let intervalID = setInterval(function[, delay, arg1, arg2, ...]);
function
:要定期執行的函數。delay
:執行間隔的時間,以毫秒為單位。arg1, arg2, ...
:傳遞給函數的額外參數。
使用示例
let counter = 0;
const intervalID = setInterval(() => {
console.log("Hello World!");
counter++;
if (counter === 5) {
clearInterval(intervalID);
}
}, 1000);
這個例子會每秒列印 "Hello World!",併在列印5次後停止
clearTimeout() 和 clearInterval()
這兩個函數用於取消由 setTimeout()
和 setInterval()
設置的定時器。
語法
clearTimeout(timeoutID)
:取消由setTimeout()
設置的定時器。clearInterval(intervalID)
:取消由setInterval()
設置的定時器。
定時器函數的使用註意
雖然 setTimeout()
和 setInterval()
提供了方便的定時執行功能,但它們並不保證精確的時間控制。
JavaScript 是單線程的,如果事件隊列中有其他任務在執行,定時器的回調可能會延遲執行。
此外,瀏覽器或者環境可能對這些函數的行為有特定的限制,如在後臺標簽頁或未激活的視窗中降低定時器的精度或延遲執行,以優化性能和電池壽命。
拓展提問:為什麼要銷毀定時器?Vue中如何銷毀定時器?React中如何銷毀定時器?
在JavaScript中,銷毀定時器是一個重要的操作,主要是為了避免不必要的資源占用和潛在的記憶體泄漏。定時器如果不被適當銷毀,可能會導致一些問題,如:
- 繼續執行不必要的操作:如果定時器觸發的函數不再需要執行,定時器仍然活躍會導致額外的計算,這可能影響程式性能。
- 記憶體泄漏:在某些情況下,定時器的回調函數可能引用了外部變數或者大型數據結構,如果定時器沒有被銷毀,這些引用關係可能導致所涉及的記憶體無法被垃圾回收,從而造成記憶體泄漏。
Vue中銷毀定時器
在Vue中,通常我們會在組件的生命周期鉤子中設置和銷毀定時器。最常見的做法是在mounted
鉤子中創建定時器,併在beforeDestroy
(Vue 2.x)或beforeUnmount
(Vue 3.x)鉤子中銷毀定時器。例如:
export default {
mounted() {
this.timer = setInterval(() => {
console.log('Interval triggered');
}, 1000);
},
beforeDestroy() { // Vue 2.x
clearInterval(this.timer);
},
beforeUnmount() { // Vue 3.x
clearInterval(this.timer);
}
}
React中銷毀定時器
在React中,定時器通常在組件的生命周期方法或者鉤子中設置和清除。使用類組件時,你可以在componentDidMount
中設置定時器,併在componentWillUnmount
中清除。如果使用函數組件和Hooks,可以在useEffect
鉤子中處理定時器:
import React, { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Interval triggered');
}, 1000);
// 清理函數
return () => clearInterval(timer);
}, []); // 空依賴數組表示這個effect只在組件掛載時運行一次
return <div>Check the console.</div>;
}
在這個例子中,useEffect
鉤子的返回函數負責清除定時器,這個函數會在組件卸載時被調用,從而確保定時器被適當銷毀。
通過這些方法,可以確保在組件或應用的生命周期結束時,相關的定時器也被正確清除,避免潛在的問題。
補充知識點:什麼是 requestAnimationFrame
?
requestAnimationFrame
是一個由瀏覽器提供的 API,用於在下一次瀏覽器重繪之前調用特定的函數,以執行動畫或其他視覺更新。
這個函數是專門為動畫和連續的視覺更新設計的,它可以幫助你創建平滑的動畫效果,因為它能保證在瀏覽器進行下一次重繪之前更新動畫幀。
requestAnimationFrame
的特點
- 高效性能:
requestAnimationFrame
會將動畫函數的執行時機安排在瀏覽器的下一次重繪之前,這樣可以保證動畫的更新和瀏覽器的繪製操作同步進行,從而減少畫面撕裂和不必要的計算和渲染,提高性能。 - 節能:相比於
setTimeout
或setInterval
,requestAnimationFrame
是更智能的,因為它會在瀏覽器標簽頁不可見時自動暫停,從而減少CPU、GPU和電力的消耗。 - 簡單的使用方式:
requestAnimationFrame
只需要一個回調函數作為參數,瀏覽器會自動計算出最適合的調用時間。
requestAnimationFrame
的使用示例
假設你想要創建一個簡單的動畫,使一個元素在水平方向上移動:
let xPos = 0;
function animate() {
xPos += 5; // 每幀向右移動5像素
element.style.transform = `translateX(${xPos}px)`; // 更新元素位置
if (xPos < 500) { // 如果元素還沒移動到500像素的位置,繼續動畫
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate); // 開始動畫
在這個示例中,animate
函數會被連續調用,每次調用都會將元素向右移動5像素,直到它達到500像素的位置。
requestAnimationFrame
在工作中應用的註意事項
requestAnimationFrame
需要在每一幀都重新調用來繼續動畫。- 如果動畫或者視覺更新不再需要,應當使用
cancelAnimationFrame
來取消回調函數的執行,避免不必要的性能消耗。 - 由於
requestAnimationFrame
的調用時間是由瀏覽器決定的,通常它的頻率會與瀏覽器的刷新率相匹配,例如大多數設備上是每秒60次(即60Hz),但這可能會因設備而異。
補充知識點:什麼是 setImmediate
?
setImmediate
是一個在 Node.js 環境中使用的函數,用於安排一個回調函數在當前事件迴圈結束後、下一次事件迴圈開始前被立即執行。
這個函數是特定於 Node.js 的,不是 Web 標準的一部分,因此在瀏覽器環境中不可用。
setImmediate
的功能和用途
setImmediate
的主要用途是將一些需要儘快執行但不必阻塞當前正在執行的操作的代碼延遲執行。它與 setTimeout
和 process.nextTick
類似,但行為略有不同:
setImmediate
安排的任務會在當前事件迴圈的“check”階段執行。setTimeout(fn, 0)
會在定時器階段執行,通常會有一小段延遲(最小延遲時間,通常是1毫秒,取決於環境)。process.nextTick
會在當前事件迴圈的任何階段結束後立即執行,甚至在進入下一個事件迴圈階段之前。
setImmediate
的使用示例
下麵是一個簡單的 Node.js 示例,演示了 setImmediate
的用法:
console.log('開始執行');
setImmediate(() => {
console.log('執行 setImmediate 回調');
});
console.log('結束執行');
在這個例子中,輸出將會是:
開始執行
結束執行
執行 setImmediate 回調
這表明 setImmediate
安排的回調確實是在當前事件迴圈的末尾執行的。
setImmediate
在工作中應用的註意事項
- 非標準 API:
setImmediate
是一個非標準的 API,只在 Node.js 環境中可用。在瀏覽器中,你可能需要使用setTimeout(fn, 0)
來達到類似的效果,雖然這兩者在行為上有細微的差別。 - 使用場景:通常用於處理長時間運行的操作後需要快速響應的場景,或者在處理完一些同步任務後需要儘快執行的非同步代碼。
補充知識點:什麼是 process.nextTick
?
process.nextTick
是 Node.js 環境中的一個函數,它用於在 Node.js 的事件迴圈的當前階段完成後、下一個事件迴圈階段開始之前,安排一個回調函數儘快執行。
這意味著無論在事件迴圈的哪個階段調用 process.nextTick
,提供的回調函數都會在當前操作完成後立即執行,但在任何I/O事件(包括定時器)或者執行其他計劃任務之前執行。
process.nextTick
的功能和用途
process.nextTick
主要用於確保在當前執行棧運行完畢後、在進行任何非同步操作之前立即處理給定的回調。
這對於處理錯誤、清理資源或者在繼續其他事件之前進行其他緊急計算是非常有用的。
與 setImmediate
的區別
儘管 process.nextTick
和 setImmediate
都用於安排非同步操作,但它們的執行時間點不同:
process.nextTick
回調在同一事件迴圈階段儘可能早地執行,即在任何I/O事件和定時器之前。setImmediate
設計為在當前事件迴圈的所有I/O事件處理完畢後執行,即在下一個事件迴圈迭代的開始。
process.nextTick
的使用示例
下麵是一個 Node.js 示例,展示了 process.nextTick
的使用:
console.log('開始執行');
process.nextTick(() => {
console.log('執行 process.nextTick 回調');
});
console.log('結束執行');
在這個例子中,輸出將會是:
開始執行
結束執行
執行 process.nextTick 回調
這表明 process.nextTick
安排的回調確實是在當前事件迴圈的末尾、在其他非同步事件之前執行的。
process.nextTick
在工作中應用的註意事項
- 遞歸調用:如果
process.nextTick
被遞歸調用,或在一個迴圈中大量調用,它可以導致I/O餓死,因為它會在處理任何I/O事件之前不斷地將新的回調加入到隊列中。 - 用途選擇:
process.nextTick
非常適合在當前操作完成後立即需要運行的情況,例如在事件或低級邏輯之後立即處理錯誤或進行清理。
框架拓展:Vue 中有用到 process.nextTick
嗎?
Vue.js 中也使用了 process.nextTick
,或者更具體地說,它使用了與之類似的非同步延遲功能。
process.nextTick
是 Node.js 的一個特性,但在瀏覽器環境中,Vue 使用的是 nextTick
方法。
這是 Vue 的全局 API,用於在下一個 DOM 更新迴圈結束後執行延遲回調。
在內部,Vue 會嘗試使用原生的 Promise.then
、MutationObserver
,或者 setImmediate
,最後退回到 setTimeout(fn, 0)
。
Vue中 nextTick
的應用
- 確保 DOM 更新完成:Vue 的數據綁定和 DOM 更新是非同步的。當你更改數據後,DOM 不會立刻更新。
nextTick
允許你在 DOM 更新完成後立即運行回調函數,這對於 DOM 依賴的操作非常有用。 - 解決狀態更新問題:有時候,你可能在同一方法中多次更改數據,使用
nextTick
可以確保所有的 DOM 更新都完成後再執行某些操作。
Vue中 nextTick
的使用示例
new Vue({
el: '#app',
data: {
message: 'Hello'
},
methods: {
updateMessage() {
this.message = 'Updated message';
this.$nextTick(() => {
// 這個回調將在 DOM 更新後執行
// `$nextTick()` 用來確保 `console.log('DOM updated')` 的執行發生在 DOM 真正更新之後
console.log('DOM updated');
});
}
}
});
補充知識點:什麼是 MutationObserver
?
MutationObserver
是一個強大的 Web API,用於監視 DOM(文檔對象模型)的變化。
當 DOM 元素被添加、刪除或修改時,MutationObserver
可以被用來非同步地通知這些變化,使開發者能夠響應這些變化並執行相應的操作。
MutationObserver
的功能
MutationObserver
主要用於監視以下類型的 DOM 變化:
- 子節點的添加或刪除。
- 屬性的添加、刪除或修改。
- 文本內容的變更。
- 更多其他類型的 DOM 變化。
MutationObserver
的用途
這使得 MutationObserver
在開發複雜的 Web 應用時非常有用,特別是在需要響應 DOM 變化來執行某些操作的情況下,如動態內容的載入、用戶界面的自動更新等。
如何使用 MutationObserver
要使用 MutationObserver
,你需要創建一個觀察者實例,定義一個回調函數來處理變化,然後指定要監視的 DOM 節點和具體的觀察選項。
MutationObserver
的簡易示例
// 監視目標節點
const targetNode = document.getElementById('some-id');
// 配置觀察選項:
const config = { attributes: true, childList: true, subtree: true };
// 當觀察到變動時執行的回調函數
const callback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
} else if (mutation.type === 'attributes') {
console.log(`The ${mutation.attributeName} attribute was modified.`);
}
}
};
// 創建一個觀察者對象並傳入回調函數
const observer = new MutationObserver(callback);
// 開始觀察已配置的變動
observer.observe(targetNode, config);
// 之後,你可以停止觀察
// observer.disconnect();
MutationObserver
在工作中應用的註意事項
- 性能考慮:雖然
MutationObserver
是非同步的,但過度使用或監視大量的 DOM 變化仍可能影響性能。合理配置觀察選項,只監視必要的變化,可以幫助避免性能問題。 - 記憶體管理:使用
MutationObserver
時應確保在不需要時斷開觀察(使用disconnect
方法),以避免記憶體泄漏。
面試問題合集
恭喜你耐心看完本文了,對照下方的問題列表,自我提問一下吧~
什麼是 事件迴圈?
事件迴圈 的執行順序是什麼?
什麼是 巨集任務和微任務?
巨集任務和微任務 有什麼區別?
點擊和鍵盤事件 是巨集任務嗎?
什麼是Promise
對象?
如何手寫一個簡易的Promise
對象?
為什麼Promise
比setTimeout
快?
Promise.all
和Promise.race
有什麼區別?
什麼是requestAnimationFrame
?
什麼是setImmediate
?
什麼是process.nextTick
?
Vue 中有用到process.nextTick
嗎?
什麼是MutationObserver
?
Vue中如何銷毀定時器?React中如何銷毀定時器?為什麼要銷毀定時器?
我是 fx67ll.com,如果您發現本文有什麼錯誤,歡迎在評論區討論指正,感謝您的閱讀!
如果您喜歡這篇文章,歡迎訪問我的 本文github倉庫地址,為我點一顆Star,Thanks~