前言
Redux
會(huì)用但又好像不知所以然读规?通過(guò)敲個(gè)todolist
的例子來(lái)自己實(shí)現(xiàn)一個(gè) redux
來(lái)深刻理解它的原理吧!
完整項(xiàng)目代碼:
https://github.com/LiaPig/redux-react-todolist
一燃少、沒(méi)有 redux 的 todolist
重點(diǎn)在于 redux
邏輯束亏,所以樣式布局啥的freestyle。
這階段為止的代碼在
pure
分支阵具。
二碍遍、引入 action 概念 和 actionCreator 概念
這階段為止的代碼在
ac_dis
分支定铜。
1. 引入 action 概念
其實(shí)我們可以發(fā)現(xiàn),對(duì)于 todos
這個(gè)數(shù)據(jù)狀態(tài)來(lái)說(shuō)怕敬,修改它的操作其實(shí)就只有四個(gè):set
揣炕、add
、delete
东跪、toggle
祝沸。
但是在具體用到這三個(gè)操作的地方很多,而且很分散越庇。
那能不能使用對(duì)象
的形式來(lái)描述這些會(huì)修改狀態(tài)的操作呢? type
字段來(lái)描述這是什么操作(set
奉狈、add
卤唉、delete
、toggle
)仁期, payload
字段來(lái)描述這操作會(huì)用到的具體參數(shù)
桑驱。
因此,一個(gè) action
就形如:
{
type: '操作名稱',
payload // 操作用到的參數(shù)
}
那么對(duì)應(yīng)的操作就可以改寫為:
// 頁(yè)面加載獲取 localstorage 時(shí)更新 todos
const setAction = {
type: 'set',
payload // 將會(huì)是一個(gè)todos數(shù)組
}
// 新增一個(gè) todo
const addAction = {
type: 'add',
payload // 將會(huì)是一個(gè)todo對(duì)象
}
// 刪除一個(gè) todo
const deleteAction = {
type: 'delete',
payload // 將會(huì)是一個(gè)id
}
// 切換某個(gè) todo 的完成狀態(tài)
const toggleAction = {
type: 'toggle',
payload // 將會(huì)是一個(gè)id
}
2. 引入 actionCreator 概念
那既然 action
都是個(gè)對(duì)象跛蛋,且 key
都固定為只有 type
和 payload
熬的。那機(jī)智的我們是不是可以寫一個(gè)函數(shù)
,函數(shù)接收一個(gè)參數(shù) payload
赊级,然后返回一個(gè) action
對(duì)象呢押框?—— 這就是 actionCreator
。
那么我們就來(lái)改寫成三個(gè) actionCreator
:
function createAdd(payload) {
return {
type: 'add',
payload // 將會(huì)是一個(gè)todo對(duì)象
}
}
function createDelete(payload) {
return {
type: 'delete',
payload // 將會(huì)是一個(gè)id
}
}
function createToggle(payload) {
return {
type: 'toggle',
payload // 將會(huì)是一個(gè)id
}
}
三理逊、引入dispatch概念
這階段為止的代碼在
ac_dis
分支橡伞。
那么,我們可以寫一個(gè)函數(shù) dispatch
晋被,讓它作為事件的中心兑徘,只有通過(guò)它才能調(diào)用對(duì)應(yīng)的操作,讓修改數(shù)據(jù)狀態(tài)的具體邏輯就只能在它里面寫羡洛,方便修改維護(hù)挂脑。
const dispatch = (action) => {
const { type, payload } = action;
switch (type) {
case 'set':
// 執(zhí)行更新todos列表的操作
break:
case 'add' :
// 執(zhí)行add一個(gè)todo的操作
break;
case 'delete' :
// 執(zhí)行delete一個(gè)todo的操作
break;
case 'toggle' :
// 執(zhí)行toggle一個(gè)todo的complete屬性的操作
break;
default:;
}
}
四、引入 bindActionCreators 概念
這階段為止的代碼在
bindActionCreators
分支欲侮。
綜上所述崭闲,我們可以觀察到,要調(diào)用修改 todos
數(shù)據(jù)狀態(tài)修改的四個(gè)操作都是以下形式:
// 修改todos
dispatch(createSet(todos));
// 新增一個(gè)todo
dispatch(createAdd(todo));
// 刪除一個(gè)todo
dispatch(createAdd(id));
// 切換一個(gè)todo的complete屬性
dispatch(createToggle(id));
1. 接收參數(shù)
那么威蕉,是不是也可以寫一個(gè)函數(shù) bindActionCreators
镀脂,讓它接收兩個(gè)參數(shù):
- 一個(gè)是用來(lái)生成
createSet(todos)
這一塊的(有時(shí)候可能不止需要一個(gè)呢,所以用對(duì)象
來(lái)表示忘伞,key
為自定義這個(gè)操作的名字薄翅,value
為對(duì)應(yīng)的actionCreator
) - 另外一個(gè)是
dispatch
函數(shù)
function bindActionCreators({ ‘自定義的操作名如addTodo’, createTodo }, dispatch) {
// 這里先省略沙兰,重點(diǎn)看接收的參數(shù)與格式
}
2.函數(shù)返回值
因?yàn)榻鈽?gòu)語(yǔ)法的便利性,我們可以將返回值也定義為一個(gè) 對(duì)象
, 對(duì)象中的 key
就為函數(shù)接收第一個(gè)參數(shù)對(duì)象里的 key
翘魄,返回對(duì)象的 value
就為一個(gè)函數(shù)鼎天,之后通過(guò)調(diào)用這個(gè)函數(shù)就可以幫我們實(shí)現(xiàn) dispatch(createAdd(todo))
。
function bindActionCreators({ ‘自定義的操作名如addTodo’, createAdd }, dispatch) {
const result = {
‘自定義的操作名如addTodo’: function valueFunc(...args) {
const action = createAdd(...args)
dispatch(createAdd(action)
}
};
return result;
}
3.如何調(diào)用這個(gè)函數(shù)
知道函數(shù)的輸入暑竟、輸出之后斋射,我們可以推測(cè)到,調(diào)用的格式為(還是要添加一個(gè)待辦的場(chǎng)景):
const { addTodo } = bindActionCreators({ addTodo: createAdd }, dispatch);
addTodo({ id: '~~~', text: '~~~', complete: false })
4.優(yōu)化bindActionCreators函數(shù)
就像在 List
組件里但荤,同時(shí)會(huì)用到 toggle
和 delete
操作罗岖,就可以優(yōu)化讓 bindActionCreators
能返回多個(gè)操作:
const bindActionCreators = (actionCreators, dispatch) => {
const result = {};
for(let key in actionCreators) {
result[key] = function (payload) {
const actionCreator = actionCreators[key];
const action = actionCreator(payload);
dispatch(action);
}
}
return result;
}
五、引入 reducer 概念 和 combineReucers 概念
這階段為止的代碼在
reducer
分支腹躁。
1. 引入 Reducer 概念
之前的例子一直就只有一個(gè) state
(todos
) 桑包。但項(xiàng)目中往往不可能這么簡(jiǎn)單,所以再新加一個(gè) incrementCount
纺非,每新加一個(gè)todo
就加一哑了,只增不減。那么就在 add
操作中會(huì)調(diào)用到烧颖。
于是我們可以發(fā)現(xiàn)到弱左,當(dāng) state
變多的時(shí)候,要根據(jù) action
操作更新 state
的步驟似乎會(huì)變得混亂炕淮。( dispatch
中根據(jù) action
即要修改 todos
的值拆火,也要修改 incrementCount
的值)。
所以我們?cè)O(shè)想有一個(gè)函數(shù) reducer
涂圆,它接收兩個(gè)參數(shù):
- 一個(gè)是 state 的值
- 另一個(gè)是即將要發(fā)生的 action
函數(shù) reducer
的返回值就為經(jīng)過(guò)這個(gè) action
操作后 state
要改變的新的值榜掌。
// todosReducer為:
const todosReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case 'set':
return payload;
case 'add':
return [...state, payload];
case 'delete':
// 簡(jiǎn)潔代碼起見(jiàn),此處省略了具體返回
case 'toggle':
// 簡(jiǎn)潔代碼起見(jiàn)乘综,此處省略了具體返回
default:
return state;
}
}
// incrementReducer為:
const incrementReducer = (state, action) => {
const { type } = action;
switch (type) {
case 'add':
return state + 1;
default:
return state;
}
}
2. 引入 reducers 概念
我們可以發(fā)現(xiàn)憎账,當(dāng)前 todolist
的 action
始終只有那四個(gè)(set
、add
卡辰、delete
胞皱、toggle
)。每一個(gè) state
都應(yīng)該有一個(gè) reducer
來(lái) 根據(jù) action
做出相應(yīng)的值改變九妈。但是要為每一個(gè) state
都寫一個(gè) reducer 太麻煩了反砌,而且重復(fù)代碼非常多。
所以我們可以用一個(gè) reducers
對(duì)象萌朱,來(lái)專門描述不同 state
根據(jù)不同 action
要做出的值改變宴树。可以讓 key
為 state
名晶疼,value
為一個(gè)函數(shù)酒贬,函數(shù)的返回值就為根據(jù)這個(gè) action
改變后的 state
的值又憨。
const reducers = {
todos: (state, action) => {
const { type, payload } = action;
switch (type) {
case 'set':
return payload;
case 'add':
return [...state, payload];
case 'delete':
return state.filter(item => item.id !== payload)
case 'toggle':
const newTodo = [...state];
const index = newTodo.findIndex(item => item.id === payload); // 把原來(lái) id 換成 payload
newTodo[index].complete = !newTodo[index].complete;
return newTodo;
default:
return state;
}
},
incrementCount: (state, action) => {
const { type } = action;
switch (type) {
case 'add':
return state + 1;
default:
return state;
}
}
};
3. 引入 combineReducer 概念
dispatch
函數(shù)的作用是根據(jù) action
來(lái)改變 state
的值。那么有了 reducer
后锭吨,原本在 dispatch
函數(shù)里的 state
具體如何發(fā)生變化已經(jīng)不再需要 dispatch
函數(shù)去關(guān)注了蠢莺,我們可以調(diào)用 reducer
函數(shù),獲取到它返回的新 state
值零如。而dispatch
函數(shù)只需要觸發(fā)更新就行了躏将。
我們假設(shè)通過(guò) reducer
能獲取到根據(jù)這個(gè) action
操作后所有修改后的 state
值的集合,一個(gè)大對(duì)象 states
考蕾。( key
為 state
的名字祸憋,value
為新的值)然后遍歷這個(gè) states
,去為每一項(xiàng) state
都去執(zhí)行它的 setter
函數(shù)肖卧,從而去更新值(無(wú)論值有沒(méi)改變蚯窥,都去調(diào)用 setter
。useState
有做這個(gè)的性能優(yōu)化喜命,所以不用擔(dān)心性能)
于是,我們先嘗試修改 dispatch
函數(shù)的代碼:
const dispatch = useCallback((action) => {
// 將所有的state茬祷,封裝在一個(gè)大的 states 對(duì)象里奖地,key萌丈、value都為state
const states = {
todos,
incrementCount
};
// 將所有 state 的 setter,封裝在一個(gè)大的 setters 對(duì)象里牌里,key 名為 state 的名,value 為對(duì)應(yīng)的 setter
const setters = {
todos: setTodos,
incrementCount: setIncrementCount
};
// 根據(jù)傳入的 action务甥,去調(diào)用 reducer 函數(shù)牡辽,獲取到返回的修改后的 states 值
const newStates = reducer(states, action);
// 循環(huán) states 大對(duì)象,更新里面的state
for (let key in newStates) {
setters[key](newStates[key]);
}
}, [todos, incrementCount])
這個(gè)時(shí)候敞临,我們發(fā)現(xiàn)态辛,之前我們寫的 reducers
并不符合 dispatch
函數(shù)想要的格式呀。于是挺尿,我們需要一個(gè)轉(zhuǎn)換函數(shù) combineReducers
(作用是將 reducers
轉(zhuǎn)換為 dispatch
想要的 reducer
) 奏黑,它接收一個(gè)參數(shù) reducers
,返回值就為我們 dispatch
函數(shù)里想要的 reducer
格式 —— 是一個(gè)函數(shù)编矾,第一個(gè)參數(shù)為所有 state
的集合 states
熟史,第二個(gè)參數(shù)為 action
;返回值為更新的所有 newState
的集合 newStates
窄俏。
// 創(chuàng)建一個(gè) combineReducers 函數(shù)蹂匹,讓它能返回 reducer 函數(shù)
const combineReducers = (reducers) => {
// 為了更形象的表示,沒(méi)有使用箭頭函數(shù)
return function reducer(states, action) {
// 經(jīng)過(guò)這個(gè) action 操作凹蜈,包含了所有改變了的 state 值的 states 對(duì)象
const changedStates = {};
// reducers 的 key 都為 state 名
for (let key in reducers) {
changedStates[key] = reducers[key](states[key], action);
}
// 別忘了 reducer 的返回值是一個(gè)經(jīng)過(guò) action 處理后的 states 值
return {
...states,
...changedStates
}
}
};
最后我們發(fā)現(xiàn)限寞,reducers
和 combineReducers
應(yīng)該是獨(dú)立的忍啸,我們?cè)?todolist
中,想要的就只有通過(guò) combineReducers(reducers)
轉(zhuǎn)換后的 reducer
昆烁。所以可以將 reducers
和 combineReducers
放在一個(gè)名為 reducers.js
的文件中吊骤,最后導(dǎo)出 combineReducers(reducers)
,在 todolist
中引入這個(gè)文件即可静尼。
// reducers.js
const reducers = { // 此處省略具體白粉,詳情可看項(xiàng)目代碼 };
const combineReducers = (reducers) => { // 此處省略具體,詳情可看項(xiàng)目代碼 };
export default combineReducers(reducers);
// App.jsx
import reducer = './reducers.js'
六鼠渺、引入異步 Action 概念
這階段為止的代碼在
reducer
分支鸭巴。
之前都是同步的操作,那如果在異步的場(chǎng)景拦盹,如何拿到正確的 state
呢鹃祖?
先來(lái)模擬一下異步的場(chǎng)景,看看 state
是否是實(shí)時(shí)的普舆。假設(shè)要新增一條 todo
不是同步的恬口,而是異步的(使用定時(shí)器來(lái)模擬)。然后還要判斷判斷現(xiàn)有的 todos
里有沒(méi) text
相同的沼侣,如果沒(méi)有祖能,才新增:
- 首先,之前的整個(gè)要新增的
todo
數(shù)據(jù)蛾洛,都在Control
組件里生成的养铸,我們需要修改一下,只提供text
字段就可以了:
addTodo({
text: newText,
});
- 將新增
todo
的actionCreator
(即CreateAdd
)改為異步action
轧膘,使用定時(shí)器钞螟,然后在回調(diào)函數(shù)里加入判斷text
,沒(méi)有相同的才dispatch
這個(gè)action
谎碍。所以可以把CreateAdd
的返回改為函數(shù)
鳞滨,接收兩個(gè)參數(shù),一個(gè)是dispatch
蟆淀,一個(gè)是所有state
的states
太援,返回dispatch(action)
:
export const createAdd = (payload) => {
// return {
// type: 'add',
// payload
// }
return (dispatch, getStates) => {
setTimeout(() => {
const { todos } = getStates()
if (todos.findIndex(item => item.text === payload.text) === -1) {
dispatch({
type: 'add',
payload: {
id: Date.now(),
text: payload.text,
complete: false
}
})
}
}, 5000)
}
}
- 接下來(lái)再修改
dispatch
函數(shù),在它調(diào)用reducer
函數(shù)邏輯之前扳碍,先判斷action
的類型提岔,如果action
的類型是函數(shù)(異步Action
),直接調(diào)用它且把dispatch
和states
傳給它笋敞,最后一定要記得return
碱蒙,不再執(zhí)行之后的操作。
if (typeof action === 'function') {
action(dispatch, states)
return
}
- 接下來(lái)實(shí)操一下,在
todos
里只有一條text
為 "aaa" 的場(chǎng)景下赛惩,然后再新增一條text
為"aaa"
的todo
哀墓,在敲下回車后,立馬又把原來(lái)的todo
刪掉喷兼。
按道理來(lái)說(shuō)篮绰,我們想要的結(jié)果是五秒后 todos
有一條 "aaa"
的 todo
。
然而季惯,五秒過(guò)去了吠各,todos
空空如也。
- 查閱代碼發(fā)現(xiàn)勉抓,原來(lái)在五秒后的回調(diào)函數(shù)里贾漏,我們拿到的
states
,是五秒前就已經(jīng)傳進(jìn)來(lái)的states
藕筋。那時(shí)候纵散,todos
里有一條text
為"aaa"
的todo
。所以在回調(diào)函數(shù)的判斷里隐圾,以為已經(jīng)有了伍掀,所以不會(huì)新增。那么我們嘗試把參數(shù)states
改為getStates
函數(shù)暇藏,然后在回調(diào)里再調(diào)用再獲取states
蜜笤。
修改 createAdd
這個(gè)異步的 actionCreator
里的代碼:
export const createAdd = (payload) => {
// 參數(shù)該為getStates
return (dispatch, getStates) => {
setTimeout(() => {
// 五秒后再去獲取states里的todo
const { todos } = getStates()
if (todos.findIndex(item => item.text === payload.text) === -1) {
dispatch({
type: 'add',
payload: {
id: Date.now(),
text: payload.text,
complete: false
}
})
}
}, 5000)
setTimeout(() => {
console.log('已經(jīng)五秒啦')
}, 5000)
}
}
配合修改 dispatch
函數(shù)關(guān)于異步 Action
傳參的代碼:
if (typeof action === 'function') {
action(dispatch, () => states);
return;
}
- 再做跟步驟4一樣的實(shí)操
結(jié)果發(fā)現(xiàn),五秒過(guò)去叨咖,todos
還是空空如也瘩例。
再查閱代碼發(fā)現(xiàn)啊胶,dispatch
函數(shù)里的 states
對(duì)象甸各,總是在異步 Action
發(fā)起之前臨時(shí)聲明構(gòu)成的。五秒鐘之后焰坪,原數(shù)據(jù)的 states
趣倾,由于中途我們刪了一個(gè)todo,所以 states
已經(jīng)發(fā)生變化某饰,但通過(guò) getStates()
獲取到的 dispatch
里的 states
儒恋,還是舊的。
七黔漂、引入 store 概念
針對(duì)上面的問(wèn)題诫尽,可以猜想到,只要是在組件上下文的 states
炬守,可能都獲取不到最新值牧嫉,很有可能每次的渲染周期,返回的 states
的值都不一樣。所以酣藻,應(yīng)該把 states
都放在 App組件
之外曹洽,通過(guò) useEffect
來(lái)同步更新。
- 聲明提個(gè)
store
對(duì)象辽剧,用來(lái)存放states
const store = {
todos,
incrementCount
}
- 用
useEffect
來(lái)同步更新
useEffect(() => {
Object.assign(store, {
todos,
incrementCount
})
}, [todos, incrementCount])
- 更換
dispatch
函數(shù)里的getStates里的返回值送淆,改為store
if (typeof action === 'function') {
action(dispatch, () => store);
return;
}
- 實(shí)操,新增一條
"aaa"
的同時(shí)怕轿,刪除原來(lái)的"aaa"
偷崩。結(jié)果發(fā)現(xiàn),五秒后撤卢,新增了兩條"aaa"
环凿。
能成功新增說(shuō)明異步后拿到的 todos
是最新的。
觀察代碼發(fā)現(xiàn)放吩,五秒后在回調(diào)函數(shù)里我們 dispatch
了新的 todo
智听,然后是走dispatch
的同步 action
的邏輯,通過(guò) reducer
獲取到返回的修改后的 states
值渡紫。而我們?cè)谶@里傳給 reducer
的仍是舊的 states
值到推,所以把傳給 reducer
的也改為 store
。
然后我們還可以發(fā)現(xiàn)惕澎,在 dispatch
函數(shù)里莉测,已經(jīng)沒(méi)有使用 states
了,所以可以把參數(shù)定義去掉唧喉,還有這個(gè)函數(shù)已經(jīng)不再依賴 states
的 todos
和
incrementCount
了捣卤。所以也不再需要使用 useCallback
來(lái)包裹 dispatch
函數(shù)了。
5.再實(shí)操八孝,終于成功啦6!干跛!