前言
接上一篇,在了解了狀態(tài)轉(zhuǎn)移中如何保證不變性之后,我們又來看看如何完成多變量狀態(tài)的組合。
組合
在我們的項(xiàng)目由簡入繁的過程中,針對(duì)狀態(tài)進(jìn)行組合是個(gè)很常見的需求,比方說像我們之前實(shí)現(xiàn)的一個(gè)待辦事項(xiàng)列表的項(xiàng)目,這里面的狀態(tài)維護(hù)的實(shí)際上是一個(gè)待辦事項(xiàng)數(shù)組,數(shù)組中每個(gè)元素包含了待辦事項(xiàng)的名稱、id和是否完成狀態(tài)等等。當(dāng)我們需要針對(duì)狀態(tài)進(jìn)行過濾顯示的時(shí)候(比方說我們可能想在顯示全部和顯示未完成中切換),我們就需要有一個(gè)新的狀態(tài)來保存我們當(dāng)前顯示的模式。為了完成這一切,我們就需要將這兩個(gè)變量進(jìn)行組合形成我們新的狀態(tài),而相對(duì)應(yīng)的reducer函數(shù)也同樣需要組合。
代碼實(shí)現(xiàn)
最簡單的做法肯定是,將這兩個(gè)變量揉到一個(gè)對(duì)象中,并重新寫一個(gè)reducer函數(shù),把這兩個(gè)變量各自的業(yè)務(wù)行為邏輯也寫到一起,從代碼的層面上完成組合。這當(dāng)然可以做,但是如果每次增加一個(gè)新的變量,你都需要修改代碼,久而久之這個(gè)函數(shù)就會(huì)非常難以理解和維護(hù),而且,如果你是使用了一個(gè)第三方的庫,那有怎么辦呢?
委托復(fù)用實(shí)現(xiàn)
其實(shí)之前的做法有一半是可取的,即把兩個(gè)變量揉到一個(gè)對(duì)象中來完成狀態(tài)的組合。那么我們想,是不是只有修改原有reducer函數(shù)一條路才能夠完成我們的需求呢?我們所要的實(shí)際上是一個(gè)新的reducer函數(shù)能夠完成根據(jù)下發(fā)行為完成組合之后的狀態(tài)對(duì)象的轉(zhuǎn)化。原有的函數(shù)是否能夠通過組合來完成這些呢?答案是肯定的,以代碼來說,我們首先完成一個(gè)新的針對(duì)顯示狀態(tài)的reducer函數(shù):
const reducer = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state
}
}
export default reducer;
那么我們可以這樣來組合一個(gè)新的函數(shù):
import reducerTodo from './TodoReducer'
import reducerFilter from './VisibilityFilterReducer'
const todoApp = (state = {}, action) => {
return {
todos: reducerTodo(
state.todos,
action
),
visibilityFilter: reducerFilter(
state.visibilityFilter,
action
)
}
}
可以看到,我們這個(gè)新的reducer函數(shù)維護(hù)一個(gè)新的狀態(tài)對(duì)象,它包含兩個(gè)變量
- todos 就是我們之前的待辦事項(xiàng)列表
- visibilityFilter 是這次新加的顯示過濾狀態(tài)
新函數(shù)的對(duì)狀態(tài)的具體維護(hù)工作其實(shí)是委托給了每個(gè)變量各自的reducer函數(shù)來執(zhí)行,這個(gè)函數(shù)只是做了一個(gè)邏輯組合的工作,實(shí)現(xiàn)了代碼的復(fù)用和零修改,最大程度的保證了可維護(hù)性。
Redux實(shí)現(xiàn)
上面的做法已經(jīng)很接近Redux的實(shí)現(xiàn)了,只是Redux提供了一個(gè)更簡單的函數(shù):combineReducers來解決這個(gè)問題,這個(gè)函數(shù)接受一個(gè)對(duì)象參數(shù),代碼如下:
import {combineReducers} from Redux
const todoApp = combineReducers({
todos: reducerTodo,
visibilityFilter: reducerFilter
})
這段代碼得到的todoApp跟上面代碼所得的todoApp是等價(jià)的,也就是說他們完成相同的功能。維護(hù)一個(gè)狀態(tài)對(duì)象,這個(gè)對(duì)象包含一個(gè)待辦事項(xiàng)列表todos和顯示過濾狀態(tài)visibilityFilter,并且指定了各自的reducer函數(shù)。
我們比較好奇的是combineReducers具體做了哪些事情,先來定個(gè)架子:
const combineReducers = (reducers) => {
return (state = {}, action) => {
return ...
}
}
我們明確的是,combineReducers函數(shù)接受由多個(gè)函數(shù)組合形成的reducers對(duì)象,并返回一個(gè)新的reducer函數(shù)。這個(gè)函數(shù)的邏輯是維護(hù)一個(gè)新的state對(duì)象,這個(gè)對(duì)象每個(gè)屬性名與reducers對(duì)象的屬性名一致,并且每個(gè)屬性的狀態(tài)轉(zhuǎn)移取值由reducers對(duì)象中的同名函數(shù)決定。那么我們可以這樣實(shí)現(xiàn):
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](state[key], action)
return nextState
},
{}
)
}
}
這個(gè)代碼的邏輯是,遍歷reducers對(duì)象中所有的key值,調(diào)用reducers中對(duì)應(yīng)key的函數(shù),入?yún)⑹莝tate對(duì)應(yīng)key的變量和action,將得到的新的狀態(tài)值放回到結(jié)果對(duì)象對(duì)應(yīng)的key值中(這一切都借用了數(shù)組的reduce函數(shù),不熟悉的可以先了解一下js中的函數(shù)式編程)。
這里就體現(xiàn)出之前文章中所提到的關(guān)鍵兩點(diǎn):
- 每個(gè)reduce函數(shù)必須有一個(gè)自己的初態(tài)和default缺省處理邏輯,像上面這個(gè)例子,如果我們下發(fā)的行為是添加待辦事項(xiàng),那這個(gè)行為與過濾顯示狀態(tài)是無交集的,對(duì)于顯示狀態(tài)的reducer,這個(gè)行為是要觸發(fā)缺省的行為和初態(tài)的。
- 每個(gè)reducer函數(shù)必須是純函數(shù),不能有任何副作用并要維護(hù)狀態(tài)的不可變性,如果有一個(gè)點(diǎn)打破了這個(gè)規(guī)則,在組合的情況下,會(huì)形成龐大的狀態(tài)變量引用層級(jí),導(dǎo)致整體狀態(tài)的不可跟蹤。如果所有的reducer都遵守這個(gè)規(guī)則,那么組合到一起的狀態(tài)也必將是不可變的。
實(shí)戰(zhàn)
在了解了組合的原理和實(shí)現(xiàn)之后,我們來豐富一下我們的待辦事項(xiàng),首先編寫我們的過濾鏈接組件:
import React, { Component } from 'react';
import './App.css';
class FilterLink extends Component {
constructor(props) {
super(props);
}
render(){
if(this.props.curFilter === this.props.value){
return <span>{this.props.children} </span>
}else{
return <span><a href='#' onClick={(e)=>{e.preventDefault();this.props.onClick(this.props.value)}}>{this.props.children}</a> </span>
}
}
}
export default FilterLink;
接著在待辦事項(xiàng)中增加幾個(gè)選項(xiàng):
import React, { Component } from 'react';
import './App.css';
import FilterLink from './FilterLink'
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
text:'',
}
}
handleChange(event) {
this.setState({text: event.target.value})
}
render() {
return (
<div>
<input type="text" value={this.state.text} onChange={(e)=>this.handleChange(e)}></input>
<div>
<button onClick={() => this.props.doAdd(this.state.text)}>+</button>
</div>
<ul>
{this.props.todos
.filter(todo => {
switch (this.props.visibilityFilter) {
case 'SHOW_ALL':
return true
case 'SHOW_COMPLETED':
return todo.completed
case 'SHOW_UNCOMPLETED':
return !todo.completed
}
})
.map(todo =>
<li
style={todo.completed?{textDecoration:'line-through'}:{}}
key={todo.id}
onClick={()=>this.props.doToggle(todo.id)}>
{todo.text}
</li>
)}
</ul>
<FilterLink value="SHOW_ALL" onClick={(input)=> this.props.doFilter(input)} curFilter={this.props.visibilityFilter}>All</FilterLink>
<FilterLink value="SHOW_COMPLETED" onClick={(input)=> this.props.doFilter(input)} curFilter={this.props.visibilityFilter}>Completed</FilterLink>
<FilterLink value="SHOW_UNCOMPLETED" onClick={(input)=> this.props.doFilter(input)} curFilter={this.props.visibilityFilter}>Uncompleted</FilterLink>
</div>
);
}
}
export default Todo;
跟之前的文章差別不大,我們?cè)黾恿巳齻€(gè)按鈕來修改我們的過濾狀態(tài),并且在列表顯示部分增加了一個(gè)filter操作來過濾我們要顯示的事項(xiàng)。
然后就是基礎(chǔ)頁面:
const todoApp = combineReducers({
todos: reducerTodo,
visibilityFilter: reducerFilter
})
let store = createStore(todoApp);
let idnum = 1;
const render = () =>
ReactDOM.render(<Todo {...store.getState()}
doAdd={(input) => {store.dispatch({type: 'ADD_TODO', id: idnum++, text:input})}}
doToggle={(input) => {store.dispatch({type: 'TOGGLE_TODO', id:input})}}
doFilter={(input) => {store.dispatch({type:'SET_VISIBILITY_FILTER', filter: input})}}
/>, document.getElementById('root'));
store.subscribe(()=> render());
render();
接下來我們看看效果:


