cover

系列文章: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——每一步都在用更好的抽象解決上一代的痛點。但底層的東西從來沒消失,只是被更友善的介面包裝起來了。


延伸閱讀