[譯]從零開始Redux(三)組合

前言

上一篇,在了解了狀態(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}&nbsp;</span>
      }else{
          return <span><a href='#' onClick={(e)=>{e.preventDefault();this.props.onClick(this.props.value)}}>{this.props.children}</a>&nbsp;</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();

接下來我們看看效果:


全部

已完成

未完成
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容