
系列文章:DOM 與架構 → 本篇:非同步、CSS 與狀態管理 → API 設計與部署
非同步處理的演進:Callback → Promise → async/await
Callback Hell:末日金字塔
JavaScript 是單執行緒,但要處理大量非同步操作。最初用 callback——傳一個函式進去,操作完成後呼叫它。聽起來合理,直到你要依序執行多個非同步操作:
login(username, password, function(err, token) {
if (err) { handleError(err); return; }
getUser(token, function(err, user) {
if (err) { handleError(err); return; }
getOrders(user.id, function(err, orders) {
if (err) { handleError(err); return; }
getOrderDetail(orders[0].id, function(err, detail) {
// 終於拿到資料了...
renderOrderDetail(detail);
});
});
});
});程式碼不斷向右縮排,形成三角形。每一層重複寫 error handling。如果中間要加條件判斷或平行處理?那你不如重寫。
Promise:鏈式的救贖(ES2015)
login(username, password)
.then(token => getUser(token))
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => renderOrderDetail(detail))
.catch(err => handleError(err));不再無限向右縮排。.catch() 統一了錯誤處理。Promise.all() 讓平行執行變得優雅。
但 .then() 鏈一長還是不好讀,而且中間某一步想用前面幾步的結果,就要把值一層層傳下去。
async/await:讓非同步看起來像同步(ES2017)
async function getOrderDetail() {
try {
const token = await login(username, password);
const user = await getUser(token);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
renderOrderDetail(detail);
} catch (err) {
handleError(err);
}
}同樣的邏輯,讀起來像同步程式碼。try/catch 取代 .catch()。中間結果直接用變數接住。
async/await 是今天的絕對標準。但記住:它不是魔法——底層還是 Promise,Promise 底層還是 callback。每一層抽象都沒有消滅上一層,而是在上面蓋了更友善的介面。
CSS 的演進:原生 → Sass → CSS-in-JS → Tailwind
原生 CSS:簡單但原始
CSS 是為「文件樣式」設計的,不是為「應用程式 UI」。沒有變數(同一個品牌色散落在一百個地方)、沒有巢狀(選擇器要完整重寫)、沒有作用域(所有 CSS 全域互打)。
小專案還能忍,大專案十幾個人一起改 CSS?沒人敢改,因為你不知道改了會影響到哪裡。
Sass/Less:程式語言的能力(2006/2009)
$primary: #3b82f6;
.sidebar {
width: 260px;
.nav {
padding: 8px;
.item {
color: $primary;
&:hover { color: darken($primary, 10%); }
}
}
}變數、巢狀、mixin、函式——寫 CSS 終於有了工程感。但 Sass 沒解決最根本的問題:全域作用域。你的 .title 不管寫在哪個檔案,編譯出來還是全域的 .title。
CSS-in-JS:把 CSS 寫進 JavaScript
const Button = styled.button`
background: ${props => props.primary ? '#3b82f6' : 'white'};
padding: 8px 16px;
&:hover { opacity: 0.9; }
`;真正實現了作用域隔離——每個元件的樣式不會影響外部。動態樣式變得自然。但代價是 runtime cost——styled-components 在執行時動態生成 CSS,消耗效能。bundle size 也變大。
Tailwind:反叛式的 utility-first(2017)
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:opacity-90">
送出
</button>擁護者說:不用起名字、不用切換檔案、不會有死 CSS。反對者說:HTML 太醜、可讀性差、這不就是 inline style 嗎?
CSS Modules:折中方案
你還是寫普通 CSS,但構建工具自動把 class 名改成唯一 hash。沒有 runtime cost,也沒有全域汙染。
現在的真相是:CSS 領域沒有絕對贏家。 Tailwind、CSS Modules、styled-components 都有大量使用者。不同專案需求適合不同方案,最重要的是理解每個方案的 trade-off。
狀態管理的演進:全域變數 → Flux → Redux → 輕量方案
jQuery 時代:混沌
window.currentUser = null;
window.cartItems = [];
$('#product-123').data('quantity', 3);資料存在哪裡?哪裡方便存哪裡。幾十個元件共享資料的時候,沒人知道資料什麼時候被誰改的。
Flux:Facebook 的反思(2014)
Facebook 的「幽靈通知」bug——通知顯示有未讀訊息但打開找不到——讓他們意識到雙向資料流是問題根源。Flux 提出單向資料流:Action → Dispatcher → Store → View。
概念正確,但實作繁瑣——大量樣板程式碼。
Redux:一個 Store 統治所有(2015)
Redux 精煉了 Flux:Single source of truth、State is read-only、Reducer 是純函式。Time-travel debugging 很強大。
但最被詬病的問題:Boilerplate 太多。 加一個功能要寫 Action Type、Action Creator、Reducer、Selector——很多團隊花在寫 Redux 樣板碼的時間比寫業務邏輯還多。
更輕量的方案:同樣概念,更少儀式
Zustand 幾行搞定一個 Store:
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}不需要 Provider、不需要 Action Creator、不需要 connect。
而 React Query / TanStack Query 提出了更深層的反思:你以為的「前端狀態」,大部分其實是「伺服器狀態的快取」。使用者列表、商品資料、訂單詳情——真相在伺服器上,前端只是暫存了一份。React Query 專門處理這種 server state 的取得、快取、同步和更新。
從 Callback 到 async/await、從全域 CSS 到 CSS Modules、從全域變數到 Zustand——每一步都在用更好的抽象解決上一代的痛點。但底層的東西從來沒消失,只是被更友善的介面包裝起來了。