為什么會突然想到寫這么一個大雜燴的博文呢,必須要從筆者幾年前的一次面試說起
當時的我年輕氣盛,在簡歷上放了自己的博客地址,而面試官應該是翻了我的博客,好幾道面試題都是圍繞著我的博文來提問
其中一個問題,直接使得空氣靜止了五分鐘,然后面試官結束了這次面試,那就是:如何手寫一個簡易的Promise對象?
在這里,我也先挖個坑,給你們五分鐘思考并自己回答一下這個問題~ (答案隱藏在文章中自行查看~)
也是自從那次面試,我告訴自己,工作實戰中總結的經驗,一定要知其然知其所以然,才可以真正用好這些核心知識點,不積跬步,無以至千里
說了這么多的廢話,我們進入今天的博文正題~
目錄
- 事件循環(Event Loop)
- 事件循環的執行順序
- 宏任務(MacroTasks)和微任務(MicroTasks)
- 常見宏任務
- 常見微任務
- 宏任務和微任務的區別
- Promise 對象
- Promise 的基本概念
- 如何創建 Promise 對象
- 如何使用 Promise 對象
- Promise 的優勢
- Promise 在工作中的應用場景
- 如何快速入門上手 JavaScript 中的 Promise
- 手寫一個簡易的 Promise 對象
- 定時器函數
- setTimeout()
- setInterval()
- clearTimeout() 和 clearInterval()
- 定時器函數的使用注意
- 銷毀定時器
- 為什么要銷毀定時器?
- Vue 中如何銷毀定時器?
- React 中如何銷毀定時器?
- 補充知識點
- requestAnimationFrame
- setImmediate
- process.nextTick
- Vue中有用到 process.nextTick 嗎?
- MutationObserver
- 面試問題合集
什么是事件循環(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.xclearInterval(this.timer);},beforeUnmount() { // Vue 3.xclearInterval(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~ 😃
轉發請注明參考文章地址,非常感謝!!!