昨天我們談的是 SSOT:狀態應該只存一份原始資料,其他都是即時算出來的衍生結果。但那只解決了「資料該放在哪裡」,沒有回答另一個問題:這份原始資料,誰都可以隨手改嗎?如果畫面上任何一個角落都能直接修改它,就算只存了一份,一樣會亂。今天要談的單向資料流,就是替這份唯一的資料,規定一條它唯一能被修改的路徑。
在雙向資料繫結(Two-way Binding)中,資料模型與介面自動連動。雖然開發初期極快,但隨系統複雜度增加,會導致以下問題:
為了改善上述問題,單向資料流(UDF)強制資料遵循單一方向流動,形成一個循環:
狀態(State)→ 視圖(View)→ 使用者動作(Action)→ 狀態(State)
以代購 App 為例,當使用者要更新商品價格時:
雙向綁定模式:輸入框直接與商品物件綁定,一輸入數值,商品物件立刻被修改。如果列表頁跟詳情頁同時監聽這個物件,畫面會同時跳動,容易造成更新衝突與顯示錯誤。
單向資料流模式:
// 沿用 Day 6 的 byId 形狀
const state = {
items: {
p1: { id: 'p1', name: '藍牙耳機', price: 1200 },
p2: { id: 'p2', name: '保溫瓶', price: 600 }
}
};
// 1. 使用者輸入新價格,觸發的是一個「意圖」,不是直接改資料
function handlePriceChange(itemId, newPrice) {
dispatch({ type: 'UPDATE_PRICE', payload: { itemId, newPrice } });
}
// 2. 唯一能修改 state 的地方
function itemsReducer(state, action) {
if (action.type === 'UPDATE_PRICE') {
const { itemId, newPrice } = action.payload;
return {
...state,
items: {
...state.items,
[itemId]: { ...state.items[itemId], price: newPrice }
}
};
}
return state;
}
// 畫面只負責「看」目前的 state,沒有任何地方能繞過 dispatch 直接改 state.items.p1.price
不管畫面上有多少個地方顯示這個價格,能改到它的路只有一條,這條路徑清清楚楚寫在 itemsReducer 裡,不會有人在看不到的角落偷偷改了它。
雙向綁定在小專案裡很方便,但專案一大,「誰改了資料」就會變成除錯時最先卡住的問題。單向資料流用「單一事實來源」加上「明確的動作流」,把這個問題連根拔掉——資料只有一個地方能改,而且改的過程全程可追溯。開發時犧牲一點點便利性,換來的是系統行為的可預測性,這才是長期維護真正值錢的地方。原本天真地以為,資料反正想改就改,哪個元件方便就讓它直接動手改。我忽略的是,資料量一大、牽涉的畫面一多,「誰改了什麼、什麼時候改的」這件事,本身就是一門要花心思設計的學問,不是寫程式時可以隨便帶過的小細節。單向資料流讓我第一次意識到,控制「資料怎麼被改」跟控制「資料長什麼樣子」一樣重要,甚至更重要——因為出問題的時候,要追的不是資料本身,而是改資料的那條路徑。