前言
接上一篇,在了解了狀態轉移中如何保證不變性之后,我們又來看看如何完成多變量狀態的組合。
組合
在我們的項目由簡入繁的過程中,針對狀態進行組合是個很常見的需求,比方說像我們之前實現的一個待辦事項列表的項目,這里面的狀態維護的實際上是一個待辦事項數組,數組中每個元素包含了待辦事項的名稱、id和是否完成狀態等等。當我們需要針對狀態進行過濾顯示的時候(比方說我們可能想在顯示全部和顯示未完成中切換),我們就需要有一個新的狀態來保存我們當前顯示的模式。為了完成這一切,我們就需要將這兩個變量進行組合形成我們新的狀態,而相對應的reducer函數也同樣需要組合。
代碼實現
最簡單的做法肯定是,將這兩個變量揉到一個對象中,并重新寫一個reducer函數,把這兩個變量各自的業務行為邏輯也寫到一起,從代碼的層面上完成組合。這當然可以做,但是如果每次增加一個新的變量,你都需要修改代碼,久而久之這個函數就會非常難以理解和維護,而且,如果你是使用了一個第三方的庫,那有怎么辦呢?
委托復用實現
其實之前的做法有一半是可取的,即把兩個變量揉到一個對象中來完成狀態的組合。那么我們想,是不是只有修改原有reducer函數一條路才能夠完成我們的需求呢?我們所要的實際上是一個新的reducer函數能夠完成根據下發行為完成組合之后的狀態對象的轉化。原有的函數是否能夠通過組合來完成這些呢?答案是肯定的,以代碼來說,我們首先完成一個新的針對顯示狀態的reducer函數:
const reducer = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state
}
}
export default reducer;
那么我們可以這樣來組合一個新的函數:
import reducerTodo from './TodoReducer'
import reducerFilter from './VisibilityFilterReducer'
const todoApp = (state = {}, action) => {
return {
todos: reducerTodo(
state.todos,
action
),
visibilityFilter: reducerFilter(
state.visibilityFilter,
action
)
}
}
可以看到,我們這個新的reducer函數維護一個新的狀態對象,它包含兩個變量
- todos 就是我們之前的待辦事項列表
- visibilityFilter 是這次新加的顯示過濾狀態
新函數的對狀態的具體維護工作其實是委托給了每個變量各自的reducer函數來執行,這個函數只是做了一個邏輯組合的工作,實現了代碼的復用和零修改,最大程度的保證了可維護性。
Redux實現
上面的做法已經很接近Redux的實現了,只是Redux提供了一個更簡單的函數:combineReducers來解決這個問題,這個函數接受一個對象參數,代碼如下:
import {combineReducers} from Redux
const todoApp = combineReducers({
todos: reducerTodo,
visibilityFilter: reducerFilter
})
這段代碼得到的todoApp跟上面代碼所得的todoApp是等價的,也就是說他們完成相同的功能。維護一個狀態對象,這個對象包含一個待辦事項列表todos和顯示過濾狀態visibilityFilter,并且指定了各自的reducer函數。
我們比較好奇的是combineReducers具體做了哪些事情,先來定個架子:
const combineReducers = (reducers) => {
return (state = {}, action) => {
return ...
}
}
我們明確的是,combineReducers函數接受由多個函數組合形成的reducers對象,并返回一個新的reducer函數。這個函數的邏輯是維護一個新的state對象,這個對象每個屬性名與reducers對象的屬性名一致,并且每個屬性的狀態轉移取值由reducers對象中的同名函數決定。那么我們可以這樣實現:
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](state[key], action)
return nextState
},
{}
)
}
}
這個代碼的邏輯是,遍歷reducers對象中所有的key值,調用reducers中對應key的函數,入參是state對應key的變量和action,將得到的新的狀態值放回到結果對象對應的key值中(這一切都借用了數組的reduce函數,不熟悉的可以先了解一下js中的函數式編程)。
這里就體現出之前文章中所提到的關鍵兩點:
- 每個reduce函數必須有一個自己的初態和default缺省處理邏輯,像上面這個例子,如果我們下發的行為是添加待辦事項,那這個行為與過濾顯示狀態是無交集的,對于顯示狀態的reducer,這個行為是要觸發缺省的行為和初態的。
- 每個reducer函數必須是純函數,不能有任何副作用并要維護狀態的不可變性,如果有一個點打破了這個規則,在組合的情況下,會形成龐大的狀態變量引用層級,導致整體狀態的不可跟蹤。如果所有的reducer都遵守這個規則,那么組合到一起的狀態也必將是不可變的。
實戰
在了解了組合的原理和實現之后,我們來豐富一下我們的待辦事項,首先編寫我們的過濾鏈接組件:
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;
接著在待辦事項中增加幾個選項:
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;
跟之前的文章差別不大,我們增加了三個按鈕來修改我們的過濾狀態,并且在列表顯示部分增加了一個filter操作來過濾我們要顯示的事項。
然后就是基礎頁面:
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();
接下來我們看看效果: