現代 JavaScript 開發中,「非同步操作」幾乎無所不在,例如網路請求、定時器、讀取檔案等。如果沒有良好的非同步處理方式,程式碼很容易變得混亂、難以維護。
JavaScript 的非同步處理方式經歷了三個重要階段:- Callback(回呼函式)
- Promise
- Async / Await
本文將從最原始的 Callback 開始,逐步介紹 Promise 與 Async/Await 的演進過程,幫助你建立完整的非同步觀念,理解它們之間的差異與使用時機。
Callback
接著我們來看一下最原始的非同步函式長啥樣:
function fetchData(callback){
setTimeout(()=>{
callback("取回資料");
},2000)
}
fetchData(function(data){
console.log(data);
});
在這段範例中:
- fetchData 是一個高階函式
- 傳入的匿名函式是回呼函式
- 回呼函式會在 2000 毫秒後執行
Callback hell
用 callback 實現非同步操作乍看之下沒有什麼問題,但是當流程變多時,你的程式碼可能會變成所謂的 callback hell。假設我們要模擬一個「早上起床到出門」
以「起床到出門」的流程為例:
setTimeout(()=>{
console.log("1. 起床了");
setTimeout(()=>{
console.log("2. 刷完牙了");
setTimeout(()=>{
console.log("3. 吃完早餐了");
setTimeout(()=>{
console.log("4. 換好外出服了");
setTimeout(()=>{
console.log("5. 終於出門了")
},1000)
},1000)
},1000)
},1000)
},1000)
為何這被稱為「地獄」?
- 難閱讀:程式碼在多個非同步函式中不斷向右縮排,邏輯嵌套太深,一眼看不出流程。
- 難維護:如果現在想要在「吃完早餐」與「換好衣服」中間加入一個「整理背包」,必須要拆解中間的嵌套,非常容易出錯。
- 錯誤處理困難:如果要在每一層都加錯誤判斷(例如:牙膏沒了、早餐燒焦等),程式碼就會變得超腫超亂。
雖然這麼做還是可以達成我們想要的非同步效果,但是這種寫法長期下來會讓你非常痛苦。為了解決這樣的問題,JS 推出了 Promise。
Promise
Promise 是 ES6 引入的一個建構函式,用來表示一個「還沒有結果,但是未來一定會有結果的值」,用於處理非同步操作。
諸如網路請求、檔案讀寫、定時器等,都是 JavaScript 中的非同步操作,為了提升使用者體驗,需要一種機制來處理非同步操作的結果,Promise 就是為了因應這個問題而生,Promise 將使用 callback 的「橫向嵌套」程式碼變成了「縱向」結構。
Promise 的三種狀態
因為 Promise 是用來表示一個「尚未完成,但最後一定會得到結果」的值,所以 Promise 會有三種狀態:
- Pending: 初始的狀態,代表尚未完成或失敗
- Fulfilled: 操作成功,並返回結果
- Rejected: 操作失敗,並返回錯誤原因
一旦 promise 的狀態從 pending 變成 fulfilled 或 rejected,就不會再改變了。
建立 Promise
這邊以一個 delay function 為例,傳入毫秒做為參數,並返回一個 Promise。
function delay(ms){
return new Promise((resolve, reject)=>{
if(typeof ms !== 'number'){
reject("錯誤:請輸入數字");
return;
}
setTimeout(()=>{
resolve(`等待了 ${ms} 毫秒,執行完畢`);
}, ms);
});
}
- 用
new關鍵字建立 Promise 物件 - Promise 物件會接收一個叫做
executor的函式做為參數。 - executor 又接收
resolve、reject兩個函式做為參數,分別對應著請求成功與請求失敗時要調用的函式(executor 會在 promise 建立時立刻執行,並在適當時機呼叫resolve或reject傳)。
- 請求成功 ⭢ 調用 resolve 函式
- 請求失敗 ⭢ 調用 reject 函式
使用 Promise
有了 Promise 物件,要用 .then()、.catch()、.finally() 處理結果。
.then():當Promise 變成 Fullfilled 時執行,它可以接收 resolve 傳出的資料.catch():當 Promise 變成 Rejected 時執行,它可以接收 reject 傳出的錯誤訊息.finally():無論成功或失敗,最終都會執行。常用於關閉載入動畫
delay(1000).then(msg =>{
console.log(msg); // 等待了 1000 毫秒,執行完畢
}).catch(err=>{
console.error(err); // 錯誤:請輸入數字
}).finally(()=>{
console.log("結束");
})
resolve/reject 與 .then()/.catch() 差異
resolve/reject在 Promise 內部,用來決定 Promise 最終狀態 ⮕ 用來製造 Promise.then()/.catch()在 Promise 外部,用來處理 Promise 最終狀態 ⮕ 用來使用 Promise
Async/Await
async/await 是 Promise 的語法糖,用簡潔值觀的方式處理非同步操作,讓非同步「看起來」像是同步操作。
async
async關鍵字將函式標記為非同步,並且返回一個 Promise。
await
await是一個運算子,通常搭配 async function 一起用。(在 ES2022 之前 await 只能在 async function 中, ES2022+ 可以在 ES module 的頂層使用)- 使用 await 時會暫停
asyncfunction 執行,直到 Promise 解析完成(得到 resolved/ rejected) - await 只會暫停 async 函式內部,因此不會阻塞整個 JS 執行緒
範例 - 將使用 promise 的範例改為 async/await
async function runTimer(ms) {
try{
const result = await delay(ms);
console.log(result);
}catch(err){
console.error(err);
}finally{
console.log("結束");
}
}
runTimer(1000);
結論
JavaScript 的非同步處理方式從 Callback 發展到 Promise,再到現代常用的 Async/Await,本質上都是為了解決「等待任務完成後再繼續執行」的問題。
三者的關係可簡單理解為:
- Callback:最原始的非同步寫法,但容易產生 callback hell
- Promise:改善巢狀結構,提供相對清晰的流程與錯誤處理
- Async/Await:基於 Promise 的語法糖,讓非同步程式碼更接近同步思維
實務開發中,Async/Await 已經成為主流寫法,但理解 Callback 與 Promise 的運作原理,還是很重要,因為 Async/Await 的底層機制是建立在 Promise 之上。
參考








