原文鏈接
這是一次突發奇想的感悟,感覺還挺神奇的,遂記錄一下。
前言
作為一個React的開發者已經蠻久的了,大大小小的應用也開發了不少,除了一開始學習React時用過Redux以外,後來基本都不碰了,不管多麼複雜的應用,我也簡單的覺得使用Context就能夠解決我所有的問題。説來慚愧,我基本沒有思考過Redux存在的原因,可能是React真的做的太好了,又或者是我們現在的設備性能已經嚴重過剩了,讓我完全不需要考慮應用優化的問題。
今天又冒出為什麼這麼多人用Redux的問題,所以又看了一下React和Redux的文檔,結果有蠻大的收穫(每次看文檔都有新收穫,推薦大家沒事多看看),突然讓我回憶起曾經好多次使用useState更新數組時的彆扭(雖然沒什麼問題,但是總覺得過於複雜了),今天我們就來聊聊這些。
隨便推薦使用我寫的理解例子examples/reducer-context-redux一起服用效果更佳哦,以下所有完整代碼皆可在例子中找到。
需求
脱離真實需求聊一些技術的東西,總讓人覺得比較虛,所以我們今天就來聊一下一個比較簡單的需求,比較幾種不同的方式演變的代碼的區別來幫助我們理解Redcuer、Context和Redux這些概念。
簡單的描述一下需求,一個可以創建todo的輸入框,一個展示todo的列表,todo本身可以修改名稱,標記為完成或者刪除,具體看下圖。
一般實現
從圖片也可以看出來這是一個非常簡單的需求,讓我們來快速的實現一下,完整代碼見examples/reducer-conetxt-redux/base
...
// 定義一個 Todos
const [todos, setTodos] = useState([]);
// 定義幾個方法分別用來 創建,更新,刪除 Todo
const handleAddTodo = (name) => {
setTodos([...todos, { id: nextId++, name, done: false }]);
};
const handleChangeTodo = (todo) => {
setTodos(
todos.map((t) => {
if (t.id === todo.id) {
return todo;
} else {
return t;
}
})
);
};
const handleDelTodo = (id) => {
setTodos(todos.filter((t) => t.id !== id));
};
為什麼我只貼了一部分代碼?因為這部分的代碼將會進行第一步演化,我相信大部分的人應該都會這麼寫吧(如果不是的話,別噴我,至少我在大部分情況下都是這麼寫的)。
但是它有什麼問題呢?其實沒有什麼問題,如果你還沒有遇到問題的話,它的確沒有問題,感覺自己在説廢話呢,那我給幾個你可能會遇到問題的情況吧:
- 如果 Todo 需要通過接口來完成創建、更新和刪除,那當你同時進行多個操作時,會導致你的
todos只完成了最後一次更新。 - 關於
1有一個非常難受的地方,就是你不太能容易發現todos為什麼沒有正確更新成你期望的樣子,這個排查是很痛苦的,不知道你們有沒有遇到過? - 雖然現在
useState好像非常直觀的展示了todos的更新機制,但如果我添加更多的功能,比如多狀態的todo,這個時候你需要更多的setTodos來更新todos,這好像比較難理解,那我們可以將更新這個操作改為上一步或者下一步,這樣你是不是就需要拆開handleChangeTodo這個方法了?
好了,差不多第一版就這些,那麼如何來優化它?
Reducer
要用它,就得先知道它是什麼吧?簡單來説,就是把所有狀態更新邏輯合併到一個函數中,就叫Reducer。它的定義已經出來了,我覺得這個時候你可能已經想到了如何用Reducer來更新上面的第一版了,完整代碼見examples/reducer-context-redux/reducer。
function todoReducer(todos, action) {
switch (action.type) {
case 'added': {
return [...todos, { id: nextId++, name: action.name, done: false }];
}
case 'changed':
return todos.map((t) => {
if (t.id === action.todo.id) {
return action.todo;
} else {
return t;
}
});
case 'deleted': {
return todos.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
const [todos, dispatch] = useReducer(todoReducer, []);
const handleAddTodo = (name) => {
dispatch({
type: 'added',
name,
});
};
const handleChangeTodo = (todo) => {
dispatch({
type: 'changed',
todo,
});
};
const handleDelTodo = (id) => {
dispatch({
type: 'deleted',
id,
});
};
直觀看,代碼好像比上面更多了?這個例子確實是,但是就像我説的第一版中問題3那樣,當你的狀態越來越複雜,兩種方式帶來的代碼增長將不會一樣,也就是説,狀態多到一定的程度後,這樣寫的代碼會更少,不過這好像也不能成為一個這麼寫的充分理由。
那我在來補充幾個這樣寫的好處吧:
- 所有的狀態變更都收在了
todoReducer函數裏面,你可以方便的在這個函數裏面console.log來感知狀態的變化。 -> 方便調試 todoReducer作為一個乾淨的函數,你可以輕易的寫出它的測試用例。 -> 方便寫測試用例,增強穩定性- 狀態的變化一目瞭然。 -> 增強可讀性
當然這些都不是必要的,你完全可以按照你的喜好和場景來使用useState或者useReducer,不過你應該要知道它們的區別。
Context
上面的代碼好像和Context並沒有什麼直接的聯繫,我又為什麼要把它也拿來一起看看呢?是因為Redux就像是Reducer和Context的結合體,所以你現在已經知道了Reducer是什麼了,當然也要知道Context是什麼,和上述一樣完整代碼見examples/reducer-conetxt-redux/context
。
那麼Context又是什麼呢?簡單來講,就是兩個沒有直接聯繫的組件共享狀態。比如A -> B -> C -> D,D想要接收到A的狀態,需要經過B和C,那如果你用Context就可以跳過B和C,看起來是不是很好用?確實是,但是它有一個很大的缺點,也是我們不希望它被濫用的原因,因為你需要從A定義這個狀態,那麼如果這個狀態發生了變化,A所有的子組件都會更新,那你要是在一個特別大的應用的根上定義了一個經常變化的狀態,那這個應用就得經常更新,是一件比較可怕的事情。
再來看Context的主要目的是為了跨組件共享狀態,它並不具有狀態定義和管理的功能,也就是需要搭配useState或者useReducer來使用,這也是為什麼我説Redux就像是Reducer和Context的結合體,來看下代碼演進吧。
// TodoContext.jsx
import { createContext } from 'react';
export function todoReducer(todos, action) {
switch (action.type) {
case 'added': {
return [...todos, { id: nextId++, name: action.name, done: false }];
}
case 'changed':
return todos.map((t) => {
if (t.id === action.todo.id) {
return action.todo;
} else {
return t;
}
});
case 'deleted': {
return todos.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default createContext();
//
import TodoContext, { todoReducer } from './context';
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
<ContextApp />
</TodoContext.Provider>
);
當然這裏你依然可以用useState去取代useReducer,看你如何考慮吧!
Redux
終於到了最後一步了,其實你不用Redux,好像也不會阻塞你做這個需求,或者説任何別的需求,但是為什麼要用它,這就是今天要探究的內容了,一樣完整代碼見examples/reducer-conetxt-redux/redux
。
先看代碼改造吧,由於新的Redux使用Toolkit來組織代碼,但是為了方便理解,我還是單純的使用redux來演示這個改造。
// store.jsx
import { createStore } from 'redux';
function todoReducer(todos = [], action) {
switch (action.type) {
case 'added': {
return [...todos, { id: nextId++, name: action.name, done: false }];
}
case 'changed':
return todos.map((t) => {
if (t.id === action.todo.id) {
return action.todo;
} else {
return t;
}
});
case 'deleted': {
return todos.filter((t) => t.id !== action.id);
}
default:
return todos;
}
}
export default createStore(todoReducer);
//
import { Provider } from 'react-redux';
export default () => (
<Provider store={store}>
<ReduxApp />
</Provider>
);
其實對比Context的版本來看,一眼就能明白我説的Redux就像是Reducer和Context的結合體,那為什麼要用它呢?直接上理由吧:
- 不知道你還記得
Context的缺點嗎?狀態的變化會導致所有組件的變化,但是Redux只會影響訂閲對應狀態的組件。 - 不知道你還記得在最初版還有一個問題沒有解決,就是異步請求後的狀態更新,
Redux有很多很好用的中間件來處理這些事情,比如redux-thunk,當然你也可以自己寫,但是意義是什麼呢? - 瀏覽器插件
Redux DevTools,讓你清晰的看到各個狀態的變化。 - 脱離於不同
UI的狀態管理,比如你同時有多個應用共享一套狀態,或者説React和Vue寫的兩個應用共享一套狀態。
這就是我理解Redux最為強大的優勢吧,也就是當你沒有這些問題的時候,你完全不需要它。
總結
就像我説的,當你還沒有意識到你要不要用Redux時,你可能不太需要它,當你在思考如何組織你的狀態,或者你已經被你的狀態搞的焦頭爛額了,你可能需要考慮它了,拋開需求談技術都挺扯蛋的,你完全有足夠的時間去不斷的優化你的代碼,而不是一開始就把所有的工具集成到一個應用裏,不管它是不是真的需要,這樣你永遠也不會明白用它的意義是什麼。