Jeff的隨手筆記

學習當一個前端工程師

0%

Race condition的認識與解法

前言

在最近的專案重構過程中,我遇到了一個關於 Race Condition(競爭條件)的問題。當我找到解決方案後,分享給團隊成員時,發現這個處理方式對他們來說都是相對陌生的。這激起了團隊的興趣,他們邀請我在技術分享會上深入探討這個主題。

身為團隊的新成員,能有機會貢獻自己的經驗是很寶貴的。為了準備這次分享,我研究了相關資料,希望能夠完整地呈現這個議題。在這篇文章中,我將分享我的發現以及實務上的解決方案。

什麼是Race Condition (競態條件)

Race Condition 指的是在多個異步操作同時執行時,由於執行順序的不確定性,可能導致非預期的結果。在前端開發中,這種情況常見於:

  • 多執行緒環境(如 Web Worker)
  • 非同步程式設計(如async/await、Promise、setTimeout等)

我們用一個簡單的範例說明一下

假設某個網站的搜尋建議是透過 API 請求來獲取的:

  • 你輸入 「weather」,網站發送請求A: search?q=weather(這個請求可能較慢)。
  • 你立刻刪除並輸入 「news」,網站發送新的請求B: search?q=news(這個請求較快)。
  • 但由於網路延遲,請求 B 先返回結果,接著請求 A 才返回。
  • 如果網站沒有適當處理請求順序,最終顯示的搜尋建議可能是「weather」,但你的搜尋框其實已經輸入「news」。

那既然知道了什麼是Race Condition,那要怎麼解決這個問題呢?

如何解決Race Condition

當時遇到這個問題時,我依稀記得之前的助教Danny在之前鐵人賽文章有說過(已經出書了:React求職特訓營),再回去翻時果然在裡面就有講到這個。

在《React 求職特訓營》,提到了三種解決方式:

  • 主動建立變數控制
  • 使用AbortController方法
  • 不要在useEffect中fetch資料,而是使用第三方套件

主動建立變數控制

這是最基本的解決方案,概念很簡單,就像我們平常控制 Loading畫面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
setIsLoading(true); // 開始請求時設為 true

fetch('/api/data')
.then(res => res.json())
.then(data => {
// 處理資料
})
.finally(() => {
setIsLoading(false); // 請求完成時設為 false
});
}, []);

return (
<>
{isLoading && <Loading />}
...
</>
)

原來我們在日常開發中常用的載入狀態控制,就是解決 Race Condition 的其中一種方法!

使用AbortController方法

AbortController 是 JavaScript 原生 API,其目的是用於中止非同步的操作。

通常會搭配fetch或其他可取消的非同步任務使用,主要是透過AbortSignal 來控制請求的取消的。

下面是一個簡單的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');

useEffect(() => {
const controller = new AbortController(); // 建立新的 controller

fetch(`/api/search?q=${searchTerm}`, {
signal: controller.signal // 將 signal 傳入 fetch
});

return () => {
controller.abort(); // 當 effect 清理時,取消請求
};
}, [searchTerm]);
}

我們可以把整個流程分成3個部分:

  • 初始化階段
    • 建立新的 AbortController 實例
    • 自動產生內部的 signal object(負責追蹤取消狀態)
  • 請求階段
    • fetch 接收 signal 參數
    • fetch 內部綁定 signal 的 abort 事件
    • 監聽取消狀態
  • 取消階段
    • 當 abort() 被呼叫時
    • signal.aborted 狀態變為 true
    • fetch 立即停止請求並拋出 AbortError

我總結了一下它的優點與限制

優點:

  • 原生支援,不需要額外套件
  • 可以精確控制請求的取消時機
  • 適合處理快速連續的 API 請求

限制:

  • AbortController 實例無法重複使用
  • 瀏覽器支援度需要考慮(可以查看 Can I use 了解支援情況)
  • 雖然客戶端取消請求,伺服器端可能仍會繼續處理

雖然有以上的限制但不可否認還是一個很有用的技術。

不要在useEffect中fetch資料,而是使用第三方套件

這部分我就沒有研究,只能把找到的資料與大家做分享。

目前最受歡迎的兩個套件是:

  1. TanStack Query(React Query)
  2. SWR (Stale-While-Revalidate)

讓套件替你去處理這類的race condition以及一些cache的問題,也是目前許多團隊的選擇。

後記

沒想到在重構專案時遇到的 Race Condition 問題,意外地成為了一個寶貴的學習機會。透過這次的技術分享,不僅讓我更深入理解了這個議題,也讓我發現原來平常習以為常的程式碼(像是控制 loading 狀態)竟然蘊含著處理非同步競態的智慧。

最讓我印象深刻的是,這個議題引發了團隊內部的討論。每個人都分享了自己處理類似問題的經驗,讓我看到了同一個問題可以有不同的解決方案。這也提醒了我,在軟體開發中,沒有絕對的標準答案,重要的是找到最適合當前場景的解決方式。

延伸資源

https://ithelp.ithome.com.tw/articles/10327980
https://juejin.cn/post/7112699475327615006
https://www.51cto.com/article/799815.html
https://mini-ghost.dev/posts/tanstack-query-source-code-1