1. 前言

以前我對非同步的理解,大概只停留在「它會在背後幫你把很重的處理做好」這種程度,對於非同步到底做了什麼,其實沒有太深的理解。

即使被問到「為什麼一定要寫非同步?」我也只能回答:「因為如果不是非同步,畫面會等到處理結束才顯示,會變得很卡。」像這樣對「為什麼非同步是必要的」的理由一直很模糊,實際寫程式時也常常會猶豫:「這裡真的要用非同步嗎?」

所以這篇文章的目標,就是理解非同步處理實際上到底在做什麼。

2. 同步處理

先在理解非同步處理之前,說明一下同步處理。

同步處理是指:
依照由上到下的順序,一次只執行一個,前一個處理沒有結束之前,不會進到下一行

console.log('1');
heavyWork();      // 等到這裡做完
console.log('2'); // heavyWork 結束後才會執行

同步處理中呼叫耗時的處理會怎樣?

從伺服器取得資料的通訊,會因為對方的狀況和網路速度而無法預測何時結束。若用同步處理來寫,就必須等資料回來才能往下一行,通訊越慢,卡住的時間就越長。
在這段期間會出現:

  • 按按鈕沒有反應
  • 不能捲動
  • 畫面內容不會更新
  • 動畫停止

從使用者的角度來看,就像整個頁面當機了一樣。

官方文件也提到:
一次只能做一件事。因此,呼叫耗時很長的同步處理後再回來,在那段期間就無法做其他事情。

那同步處理都在什麼時候用?

實際上,我們平常寫的大部分程式碼都是同步處理。

  • 計算:算總金額或含稅價格
  • 陣列操作:用 map、filter、sort 篩選或排序列表
  • 字串處理:姓名串接、日期格式化
  • 條件分支:用 if 判斷是否已登入
  • 輸入檢查(驗證):確認電子郵件格式是否正確

這類不太耗時的處理,通常都用同步處理來寫。

因此,如果把一下子就能完成的事情也硬寫成非同步,不但不會變快,還會讓程式碼變多、變複雜,也更容易產生 bug,所以需要做好切分。

3. 非同步是什麼?

非同步程式設計是一種技術:即使啟動了可能很耗時的任務,也不會一直等待它完成,而是在任務執行期間仍能回應其他事件。當任務完成後,程式再顯示其結果。

sync_vs_async_timeline.png

用圖表示大概就是這樣

如同第 2 章引用的 MDN 內容所說,JavaScript 一次只能做一件事。
這稱為單執行緒:也就是處理流程只有一條。

因此,

像與伺服器通訊這種耗時的處理,如果用同步方式來寫,就必須等到處理結束,在那之前其他工作(例如按鈕反應、畫面更新等)都做不了,畫面就會卡住。

所以,在發出命令之後,不等待結果,先往下一個處理前進,等處理完成時再接收通知或結果。

這就是非同步處理。

console.log('1');

fetch('/api/data').then(() => {
  console.log('3:資料到了'); // 通訊結束後執行
});

console.log('2'); // 不等待通訊完成,立刻執行
// .then() 裡面寫的是通訊結束後想做的事

執行結果會是 1 → 2 → 3 的順序,而且因為在等待通訊的期間 JavaScript 不會停止,所以畫面不會卡住。

4. 實際執行看看

第 3 章舉的是通訊的例子,但通訊如果沒有伺服器就沒辦法測試。所以這裡改用只靠瀏覽器就能測試的 setTimeout 來實際跑程式。

setTimeout:在指定時間過後執行處理的函式

console.log('1');

setTimeout(() => {
  console.log('2');
}, 5 * 1000); // 5 秒後執行

console.log('3');

畫面錄製 2026-10-06 6.39.09.gif

2 最後才出現。

為什麼會這樣?

  1. console.log('1') 被執行
  2. 透過 setTimeout 請瀏覽器在 5 秒後執行 console.log('2')
  3. 不用等 5 秒,直接進到下一行 console.log('3')
  4. 5 秒到了之後,才執行 console.log('2')

順帶一提,非同步處理中背後實際在運作的部分可以理解成是瀏覽器在幫忙做,因為不太好想像,所以我畫成了圖

async_setTimeout_flow.png

那 0 秒呢?

順帶一提,如果把等待時間改成 0 秒 會怎樣呢?

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0); // 0 秒後執行

console.log('3');

畫面錄製 2026-10-06 6.45.24.gif

即使是 0 秒,2 也還是最後才被處理。

傳給 setTimeout 的處理,即使時間是 0 秒,也仍然會作為非同步處理來處理。因此它會等到目前正在執行的同步處理(直到 console.log('3') 為止)全部結束後才執行。

所以,如果把不需要時間的處理寫成非同步,可能會出現資料已經拿到了,但畫面卻以空白狀態顯示的 bug,要特別注意。

一個 bug 的例子

let data;

fetch('/api/data')
  .then((res) => res.json())
  .then((json) => {
    data = json; // 通訊結束後,這裡才會有值
  });

console.log(data); // 不等通訊完成就先執行,所以是 undefined

修正方式有:

  • 把使用結果的處理寫在非同步處理裡面
  • 用 await 等到接收到結果

await 會在下一章介紹。

5. Promise 與 async/await 的說明

到這裡為止,我們已經知道非同步處理的方式是:發出命令之後,不等待結果,先繼續下一個處理,等處理完成後再接收通知或結果。接下來要談的是,如何接收處理結果。

第 4 章也舉過一個例子:如果在結果送達前就先使用值,就會變成空值。為了安全地使用已到達的結果,常見的寫法就是
Promise 和 async/await。

Promise 是什麼

MDN 對 Promise 的說明如下:

Promise 是由非同步函式回傳的物件,代表操作目前的狀態。當 Promise 回傳給呼叫端時,操作通常還沒有完成,但 Promise 物件提供了用來處理最終成功或失敗的方法。

只看這段對初學者來說還是不太容易懂……
MDN 其實還有更容易理解的說明;如果你看完「Promise 的用法」還是覺得卡卡的,會比較推薦去讀那一段。

簡單來說,Promise 是:

用來在之後接收非同步處理結果的物件,結果可以是成功或失敗及其值

Promise 有三種狀態:

狀態 意義
待定(pending) 還沒有結果
已完成(fulfilled) 成功了 → 會呼叫 .then()
已拒絕(rejected) 失敗了 → 會呼叫 .catch()

.then() 是在 Promise 變成「成功」時會被呼叫的處理註冊方法。

// 程式碼裡沒有直接寫 Promise,所以有點難看出來,
// 但呼叫 fetch() 時,其實會回傳 Promise
console.log('1');

fetch('/api/data').then(() => {  // fetch 會回傳 Promise,這就是 Promise
  console.log('3:資料到了'); // 通訊結束後執行
});

console.log('2'); // 不等待通訊完成,立刻執行
// .then() 裡面寫的是通訊結束後想做的事

補充說明

寫成 new Promise(...) 的話,也可以自己建立 Promise。不過在實務上,我們大多是使用像 fetch 這種一開始就會回傳 Promise 的函式。

接下來介紹另一種接收處理結果的方式:async/await。

什麼是 async/await

如果一直把 .then() 串下去,程式會變得不容易閱讀,所以就會使用 async/await。

async 關鍵字可以讓非同步 Promise 程式以更簡潔的方式運作。(略)因此即使使用非同步函式,也能寫出看起來像同步程式的程式碼。

  • async:加在函式前面,會讓這個函式變成非同步函式
  • await:加在 Promise 前面,會等待結果出來,並取得內容
async function getData() {
  const response = await fetch('/api/data'); // 等到結果出來為止
  console.log('資料到了');
}

這樣就不需要 .then(),可以像同步處理一樣由上往下閱讀。

不過一聽到 await 會「等待」,就會讓人懷疑:這不就跟同步處理一樣會把畫面卡住嗎?所以我實際驗證了一下。

我比較了兩種情況:一種是每 0.5 秒顯示一次計數,同時用 await 等 5 秒;另一種是用同步處理等 5 秒。
如果等待期間計數還持續增加,就代表沒有卡住;如果計數停止,就代表畫面被卡住了。

// 每 0.5 秒顯示一次計數(7 秒後停止)
let count = 0;
const timer = setInterval(() => {
  count++;
  console.log(`計數 ${count}`);
  if (count >= 14) clearInterval(timer);
}, 500);

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function getData() {
  console.log('2:開始等待');
  await wait(5 * 1000);
  console.log('4:5 秒到了');
}

console.log('1');
getData();
console.log('3');

畫面錄製 2026-10-07 15.02.57.gif

執行結果如下:

1
2:開始等待
3            // 不用等 5 秒就立刻顯示
4:5 秒到了   // 5 秒後顯示

在 2:開始等待 之後,等 5 秒的期間,3 和計數仍然持續顯示。
也就是說,被 await 暫停的只有 getData 裡面,而 JavaScript 本身還是繼續往下執行。

如果用同步處理來寫

// 每 0.5 秒顯示一次計數(7 秒後停止)
let count = 0;
const timer = setInterval(() => {
  count++;
  console.log(`計數 ${count}`);
  if (count >= 14) clearInterval(timer);
}, 500);

function waitSync(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) {}
}

function getDataSync() {
  console.log('2:開始等待');
  waitSync(5 * 1000);
  console.log('4:5 秒到了');
}

console.log('1');
getDataSync();
console.log('3');

畫面錄製 2026-10-07 15.05.48.gif

結果是 1 → 2 →(卡住 5 秒)→ 4 → 3。
在 2:開始等待 之後,5 秒內什麼都沒有顯示,計數也停止了。
這代表 JavaScript 5 秒都被 waitSync 佔住,完全做不了其他事。
如果這是 Web 應用程式,使用者端看到的就是畫面卡住、什麼都不能操作的狀態。

6. 結論:到底該用同步還是非同步?

  • 計算、陣列操作、條件分支等一下就結束的處理:同步處理
  • 通訊等需要等待結果回來的處理:非同步處理

7. 總結

  • JavaScript 一次只能做一件事(單執行緒)
  • 像通訊這種耗時的處理如果用同步方式寫,在那段時間畫面會卡住
  • 非同步處理是把耗時的處理交給瀏覽器去做,讓程式在等待期間也不會停住的機制

參考文獻

https://developer.mozilla.org/ja/docs/Learn_web_development/Extensions/Async_JS/Introducing
https://developer.mozilla.org/ja/docs/Learn_web_development/Extensions/Async_JS/Promises
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://developer.mozilla.org/ja/docs/Learn_web_development/Extensions/Async_JS/Promises#async_%E3%81%A8_await

株式会社シンシア

在 xincere 株式會社 中,我們有聘用沒有實務經驗的工程師與學生工程師實習生,一起工作。
※ 關於在シンシア的工作方式,請參考這裡

在シンシア,每年大約有 100 位沒有實務經驗的人投遞履歷並參加技術面試。


原文出處:https://qiita.com/koxrtx/items/184e06cd9744f94d4f3e


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝46   💬1  
693
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登