Day 17 我已經摸到門了:setTimeout 預約一件事,程式不會停下來等。那個「排到待辦清單最後面」的機制,今天變成主角。
現實中有些事天生就要等:跟伺服器要資料、讀一個大檔案、等使用者回應。如果程式「停在那裡等」,整個畫面會凍住。所以 JavaScript 的做法是:先去做別的事,等結果來了再回頭處理 —— 這就是非同步(asynchronous)。
最原始的寫法是「做完之後,執行我給你的這個函式」:
console.log("開始煮飯");
setTimeout(() => {
console.log("1. 洗米好了");
setTimeout(() => {
console.log("2. 下鍋了");
setTimeout(() => {
console.log("3. 煮好了");
// ...
}, 500);
}, 500);
}, 500);
我故意多加了兩層,寫到第六步的時候,縮排已經退到螢幕很裡面了。這個現象有個名字叫回呼地獄(callback hell),也有人叫它「末日金字塔」—— 因為程式碼的形狀真的像一座倒過來的金字塔。
而且不只難看:如果第三步出錯了要怎麼處理?每一層都得自己寫錯誤處理,又是一團亂。
Promise 是「一個未來會有結果的承諾」。我一開始不太懂它跟回呼差在哪,後來想到一個分法:
| 寫法 | 誰握著主導權 | |
|---|---|---|
| 回呼 | setTimeout(函式, 500) |
你把函式塞進去別人手裡 |
| Promise | wait(500).then(函式) |
別人先給你一張憑證,你拿著憑證再說要做什麼 |
因為主導權在你手上,所以可以一直往後串,不用一層層縮進去:
function wait(ms, message) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(message);
resolve(); // 承諾兌現
}, ms);
});
}
wait(500, "1. 洗米好了")
.then(() => wait(500, "2. 下鍋了"))
.then(() => wait(500, "3. 煮好了"))
.then(() => wait(500, "4. 上菜!"));
一樣四個步驟,縮排不再往右退,而是排成一直線。
.then 就是「然後」:這個承諾兌現之後,做這件事。而當 .then 裡面又回傳一個 Promise 時,下一個 .then 會等它也完成 —— 這就是四個步驟能接成一條線的原因。
Promise 有三種狀態:
| 狀態 | 意思 |
|---|---|
| pending | 還在處理中 |
| fulfilled | 成功了,有結果 |
| rejected | 失敗了,有原因 |
我自己記的比喻是點餐的號碼牌:你拿到的不是食物,是「等一下會有食物」的憑證。可以先去坐下(程式繼續跑),叫號了再回來拿。
(Day 17 的 setInterval 也給號碼牌,但那是「拆彈編號」—— 拿著它是為了取消;Promise 的號碼牌是「未來會兌現的憑證」,拿著它是為了領東西。同樣是號碼牌,用途相反。)
.then 串多了還是長。async/await 讓非同步的程式讀起來像同步:
async function 煮飯() {
console.log("開始煮飯");
await wait(500, "1. 洗米好了");
await wait(500, "2. 下鍋了");
await wait(500, "3. 煮好了");
await wait(500, "4. 上菜!");
console.log("全部完成");
}
煮飯();
console.log("← 這行會先出現");
await 的意思是「等這個承諾完成,再往下」。它只能寫在 async 函式裡。
關鍵是最後那行的位置:它會先出現。await 看起來像「停下來等」,但它只暫停這個函式內部,不會卡住外面的程式。這個差別是整天最重要的一句話。
跑練習 3 的時候我覺得「怎麼比練習 2 快這麼多」,明明都設 500 毫秒。
後來發現是我同時放開了兩個練習 —— 兩組煮飯流程一起開跑,輸出交錯在一起,看起來就像刷一下全出來了。
這算是自己絆倒自己,但它意外示範了非同步的特性:兩個流程可以同時進行,誰也不等誰。如果是同步的程式,第二組一定要等第一組跑完。
前面都是語法。最後這個實驗才是我今天最有感的部分。
我做了兩個按鈕,都是「等 3 秒」,但作法完全不同:
// A. 真的停下來等:用迴圈把執行緒佔住不放
const end = Date.now() + 3000;
while (Date.now() < end) { }
// B. 非同步等待:交出控制權,時間到再回來
await sleep(3000);
旁邊放第三個按鈕,點一下就在主控台印「我被點了 N 次」。實驗方式是:按下等待,然後在那 3 秒內狂點測試按鈕。
A 的結果:整個畫面凍住,點了沒有任何反應。

而我在這裡發現一件事,而且是實驗中最真實的一段:因為沒有即時反饋,我自己就停止點擊了。
這正是「卡住」最大的傷害。不是功能壞掉 —— 那些點擊其實沒有消失,它們被排進待辦清單,等執行緒空出來就會處理。壞掉的是使用者的信心:畫面沒反應,人就會認為它死了,然後放棄操作、重新整理、或直接關掉。
B 的結果:等待期間畫面完全正常,每一下點擊都即時有回應。

同樣是等 3 秒,一個讓使用者以為程式壞了,一個讓使用者知道程式還活著。
Day 7 那次無窮迴圈把我的電腦搞到風扇狂轉、瀏覽器跳出「網頁沒有回應」。今天的 A 按鈕是同一件事的縮小版 —— 只是它 3 秒後會自己結束,所以沒有那麼戲劇化。
兩者的根源都是:JavaScript 只有一條執行緒。任何一段程式只要不交還控制權,整個分頁就什麼都做不了 —— 不能點、不能捲動、不能重繪。
而 await 做的事就是主動交還控制權:「我要等一下,這段時間你先去忙別的」。
這個小遊戲沒有網路請求、不讀檔案、沒有任何需要等待的事情。今天學的東西,目前一行都用不上。
那為什麼還是花一天學?三個理由:
這是繼 Day 17 之後,第二次我學完一個東西、結論是「知道它是什麼,但這次用不到」。不過這次不一樣:計時器是判斷不適合,非同步是時候未到。
今天搞懂的事:
.then 就是「然後」async/await 讓非同步讀起來像同步;await 只暫停函式內部,不卡住外面
明天學 localStorage,讓資料在關掉瀏覽器之後還留著。