redux架構基礎

。

本文書接 從flux到redux , 是《深入淺出react和redux》為主的比較閱讀筆記。

redux架構基礎

「如果你願意限制做事方式的靈活度,你幾乎總會發現可以做得更好。」——John Carmark

redux的官方定義是:

Redux is a predictable static container for JavaScript apps.

按照作者Dan Abramov的說法,Redux名字的含義是Reducer+Flux。Reducer不是一個Redux特定的術語,而是一個電腦科學中的通用概念,很多語言和框架都有對Reducer函數的支援。就以JavaScript為例,數組類型就有reduce函數,接受的參數就是一個reducer,reduce做的事情就是把數組所有元素依次做「規約」,對每個元素都調用一次參數reducer,通過reducer函數完成規約所有元素的功能

筆者的理解是:redux既不操作dom,也不踐行MVC,而是專註於狀態管理。它就是一個體積很小且優雅的,規定了使用模式的庫。

和flux一樣,redux和react也沒有必然的聯繫。redux是flux設計哲學的又一種實現。

redux的哲學思想

single source of trues

"真相,單一的真相"

無論是計數器,還是一個什麼牛逼哄哄的聊天軟體,整個應用的的狀態來源於一個唯一的store,(sotore.getState)。

Redux並沒有阻止一個應用擁有多個Store,只是,在Redux的框架下,讓一個應用擁有多個Store不會帶來任何好處,最後還不如使用一個Store更容易組織程式碼。這個唯一Store上的狀態,是一個樹形的對象,每個組件往往只是用樹形對象上一部分的數據,而如何設計Store上狀態的結構,就是Redux應用的核心問題。

state is readonly

"狀態,只讀的狀態"

這條哲學不是讓你如何去塑造一個"不可寫"的state,而是告訴你,必須通過派發(dispatch)一個action的方法改變狀態:

let aaa=store.getState();aaa.bbb='ccc';

以上是錯誤的示範。

那麼派發action怎麼就能改變state呢?

changes are made with pure function called reducer

"改變,用reducer"

也就是說,action派發之後,響應的事件將被reducer所響應。reducer處理了邏輯之後,store.getState拿到的狀態也隨之更新。

現在看來,reduce和action都需要由開發者編寫。其中reduce接受兩個參數,返回一個全新的狀態對象:

const reducer=(preState,action)=>newState;

在《從flux到redux》一文中,我們寫了一個註冊方法:

// 註冊的回調函數包含了業務方法  CounterStore.dispatchToken = Dispatcher.register((action) =>  {      if (action.type === ActionTypes.INCREMENT) {          counterValues[action.countrCaption] ++;          CounterStore.emitChange();      }       else if (action.type === ActionTypes.DECREMENT) {           counterValues[action.counterCaption] --;           CounterStore.emitChange();       }      }    );

在redux到表述就是reducer:

const reducer=(preState,action)=>{    const {label,type}=action;    switch type(){      case ActionTypes.INCREMENT:          return {          ...preState,          [label]:preState[label]+1;        }      case ActionTypes.DECREMENT:          return {          ...preState,          [label]:preState[label]-1;        }      default:          return preState    }  }

所以reduce不負責儲存狀態,只計算狀態。

補白:pure function

函數式編程更在意結果而非過程。JavaScript作為"函數是一等公民"的語言,函數可以是參數,也可以是返回值:

// 面向過程計算1*(1+1)let a=1,b=1,c=1;let d=a+b;d*c; // 函數式編程 1*(1+1)const add=(a,b)=>a+b;const multify=(a,b)=>a*b;multify(1,add(1,1))

Reducer 函數最重要的特徵是,它是一個純函數。也就是說,只要是同樣的輸入,必定得到同樣的輸出。

純函數是函數式編程的概念,必須遵守以下一些約束。

•不得改寫參數•不能調用系統 I/O 的API•不能調用Date.now()或者Math.random()等不純的方法,因為每次會得到不一樣的結果

由於 Reducer 是純函數,就可以保證同樣的State,必定得到同樣的 View。


讓我們總結一下,假如你的頁面出現一個bug,在本該展現數據a的地方component1,錯誤出現了數據2,你可以用這個思路來debug:

redux實踐

現在用redux來第三次實現計數器。

安裝:

npm install --save react-redux

Action

actiontype的定義和flux版本一模一樣。action/index的不同在於:

const increment=(label)=>{  return {      type:ActionTypes.INCREMENT,      label      }  }  const decrement=(label)=>{  return {      type:ActionTypes.DECREMENT,      label      }  }

沒有了dispatcher:Dispatcher在flux中存在的作用就是把一個action對象分發給多個註冊了的Store,因為redux是是單一store,因此無需顯式設置dispatcher。

store

Redux庫提供的createStore函數,這個函數第一個參數代表更新狀態的reducer,第二個參數是狀態的初始值。

在Store下新建index.js

import {createStore} from 'redux';  import reducer from './Reducer';    // 初始狀態  const counterValues = {    firstCount: 0,    secoundCount: 0,    thirdCount: 0  }    var store=createStore(reducer,initValues);    export default store;

Ruducer

  // reducer處理分發邏輯  import ActionTypes from '../Action/ActionTypes';    export default (state, action) => {      const { label } = action;      switch (action.type) {          case ActionTypes.INCREMENT:              return {                  ...state,                  [label]: state[label] + 1              };          case ActionTypes.DECREMENT:              return {                  ...state,                  [label]: state[label] - 1              }          default:              return state;      }  }

在reducer中,絕對不能去修改參數中的state。

View

現在,修改所有組件放到src/view文件夾。

在ClickCounter中,我們不再區分不同組件的狀態。而是統一向store拿。初始狀態可以從store.getState()[this.props.label]拿。,每個組件往往只需要使用返回狀態的一部分數據。為了避免重複程式碼,我們把從store獲得狀態的邏輯放在getOwnState函數中,這樣任何關聯Store狀態的地方都可以重用這個函數。

在componentDidMount函數中,我們通過Store的subscribe監聽其變化,只要Store狀態發生變化,就會調用這個組件的onChange方法;在componentWillUnmount函數中,我們把這個監聽註銷掉,這個清理動作和componentDidMount中的動作對應。

// view/ClickCounter.js  import React, { Component } from 'react'  import store from '../stores'  import Actions from '../Action/index'    const styles = {   //...  }    class ClickCounter extends Component {      constructor(props) {          super(props)          this.state = this.getOwnState();      }        getOwnState=()=>{          return {              count: store.getState()[this.props.label]          }      }            // 保持store和state的同步      componentDidMount() {          store.subscribe(this.onChange);      }        componentWillUnmount() {          store.unsubscribe(this.onChange);      }        onChange = () => {          this.setState(this.getOwnState());      }        // 派發      onClickIncrementButton = () => {          store.dispatch(Actions.increment(this.props.label));      }        onClickDecrementButton = () => {          store.dispatch(Actions.decrement(this.props.label));      }        render() {            return (              <div style={styles.counter}>                  <div style={styles.label}>{this.props.label}</div>                  <button onClick={this.onClickDecrementButton}>-</button>                  <div style={styles.showbox}>{this.state.count}</div>                  <button onClick={this.onClickIncrementButton}>+</button>              </div>          )      }  }    export default ClickCounter;

再來看allCount組件,一開始也是初始化一個getOwnState,通過遍歷獲取,再通過store.subscribe來綁定事件

import React, { Component } from 'react'  import store from '../stores';    export default class extends Component {      constructor(props) {          super(props);          this.state = this.getOwnState();      }        getOwnState() {          const state = store.getState();          let sum = 0;          Object.keys(state).forEach((key) => {              if (state.hasOwnProperty(key)) {                  sum += state[key];              }          });          return { sum: sum };      }        onChange = () => {          this.setState(this.getOwnState());      }        shouldComponentUpdate(nextProps, nextState) {          return nextState.sum !== this.state.sum;      }        componentDidMount() {          store.subscribe(this.onChange);      }        componentWillUnmount() {          store.unsubscribe(this.onChange);      }        render() {          // switch          return (              <div>Total Count: {this.state.sum}</div>          );      }  }

那麼redux版的計數器功能就完成了。

容器與傻瓜

redux版計數器,組件就做兩件事:

•跟store拿狀態,初始化初始狀態•監聽store的改變,通過setState更新

這樣的設計仍然是違反單一職責原則的。我們應該考慮把組件拆分為嵌套兩部分:父組件負責跟store拿狀態,它必須有子組件才能運行,是為"容器組件",子組件負責根據props更新介面,是為不用思考的"傻瓜組件"。如下圖:

抽離這兩部分有兩個要點,就是容器組件應當是可復用的,而傻瓜組件不應有半點自身的思考,它是無狀態的(可以是函數式組件)。

// 容器組件  class WithContainer extends Component {      constructor(props) {          super(props)          this.state = this.getOwnState();      }        getOwnState=()=>{          return {              count: store.getState()[this.props.label]          }      }        componentDidMount() {          store.subscribe(this.onChange);      }        componentWillUnmount() {          store.unsubscribe(this.onChange);      }        onChange = () => {          this.setState(this.getOwnState())      }        onClickIncrementButton = () => {          store.dispatch(Actions.increment(this.props.label));      }        onClickDecrementButton = () => {          store.dispatch(Actions.decrement(this.props.label));      }        render() {          return (              <ClickCounter                  label={this.props.label}                  count={this.state.count}                  onClickIncrementButton={this.onClickIncrementButton}                  onClickDecrementButton={this.onClickDecrementButton}              />          )      }  }    export default WithContainer

傻瓜組件就是一個純函數:

// 傻瓜組件  function ClickCounter(props){      const {          label,          count,          onClickDecrementButton,          onClickIncrementButton      } = props;        return (          <div style={styles.counter}>              <div style={styles.label}>{label}</div>              <button onClick={onClickDecrementButton}>-</button>              <div style={styles.showbox}>{count}</div>              <button onClick={onClickIncrementButton}>+</button>          </div>      )  }

跨代傳值解決方案:context

當前所有組件都是單獨引入store。寫起來很冗餘。

一個應用中,最好只有一個地方需要直接導入Store,這個位置當然應該是在調用最頂層React組件的位置。在我們的ControlPanel例子中,就是應用的入口文件src/index.js中,其餘組件應該避免直接導入Store。

不讓組件直接導入Store,那就只能讓組件的上層組件把Store傳遞下來了。首先想到的當然是用props,畢竟,React組件就是用props來傳遞父子組件之間的數據的。不過,這種方法有一個很大的缺陷,就是從上到下,所有的組件都要幫助傳遞這個props。設想在一個嵌套多層的組件結構中,只有最裡層的組件才需要使用store,但是為了把store從最外層傳遞到最裡層,就要求中間所有的組件都需要增加對這個storeprop的支援,即使根本不使用它,這無疑很麻煩。

因此就要用到react的跨代傳值利器——context。

所謂Context,就是「上下文環境」,讓一個樹狀組件上所有組件都能訪問一個共同的對象,為了完成這個任務,需要上級組件和下級組件配合。

首先,上級組件要宣稱自己支援context,並且提供一個函數來返回代表Context的對象。然後,這個上級組件之下的所有子孫組件,只要宣稱自己需要這個context,就可以通過this.context訪問到它。

我們自然想到在應用頂端宣稱支援context並把store傳入。為此,我們創建一個特殊的組件——Provider。

在src下新建一個Provider.js:

import {Component} from 'react';import PropTypes from 'prop-types';  class Provider extends Component {    getChildContext() {    return {      store: this.props.store    };  }    render() {    return this.props.children;  }  }  Provider.propTypes = {  store: PropTypes.object.isRequired}  Provider.childContextTypes = {  store: PropTypes.object};  export default Provider;

然後在index.js中引入Provider和store:

import React from 'react';  import ReactDOM from 'react-dom';  import './index.css';  import App from './view/App';  import * as serviceWorker from './serviceWorker';  import Provider from './view/Provider';  import store from './stores/index';      ReactDOM.render(<Provider store={store}>      <App />  </Provider>, document.getElementById('root'));    serviceWorker.unregister();

自此,Provider成為了完全意義上的頂層組件。當然,如同我們上面看到的,Provider只是把渲染工作完全交給子組件,它扮演的角色只是提供Context,包住了最頂層的ControlPanel,也就讓context覆蓋了整個應用中所有組件。

那麼底層組件如何獲取context呢?當然是修改容器組件。以ClickCount為例:

import React, { Component } from 'react';  import PropTypes from 'prop-types';  // import store from '../stores'; 不再需要引入  import Actions from '../Action/index';    /*  為了讓WithContainer能夠訪問到context,必須給WithContainer類的contextTypes賦值和Provider.childContextTypes一樣的值,兩者必須一致,不然就無法訪問到context,  */  WithContainer.contextTypes = {      store: PropTypes.object  }

然後就可以用this.context.store來獲取store了。

class WithContainer extends Component {    /*    在調用super的時候,一定要帶上context參數,這樣才能讓React組件初始化實例中的context,不然組件的其他部分就無法使用this.context。    */    constructor(props, context) {        super(props, context)        this.state = this.getOwnState();    }      getOwnState = () => {        return {            count: this.context.store.getState()[this.props.label]        }    }      componentDidMount() {        this.context.store.subscribe(this.onChange);    }      componentWillUnmount() {        this.context.store.unsubscribe(this.onChange);    }      onChange = () => {        this.setState(this.getOwnState())    }      onClickIncrementButton = () => {        this.context.store.dispatch(Actions.increment(this.props.label));    }      onClickDecrementButton = () => {        this.context.store.dispatch(Actions.decrement(this.props.label));    }      render() {        return (            <ClickCounter                label={this.props.label}                count={this.state.count}                onClickIncrementButton={this.onClickIncrementButton}                onClickDecrementButton={this.onClickDecrementButton}            />        )    }}

在本文中,我們學習了redux的哲學,從框架原理層面了解了如何用redux來完成React應用,並提供優化方案——第一是把一個組件拆分為容器組件和傻瓜組件,第二是使用React的Context來提供一個所有組件都可以直接訪問的Context,也不難發現,這兩種方法都有套路,完全可以把套路部分抽取出來複用,這樣每個組件的開發只需要關注於不同的部分就可以了。

實際上本文到目前為止,從來沒講什麼react-redux。實現的所有思路都是手擼。

實際上,已經有這樣的一個庫來完成這些工作了,這個庫就是react-redux。

終極解決方案:react-redux

首先是安裝react-redux:

npm i react-redux -S

redux將實現兩個重要的功能:

•connect:鏈接容器組件和傻瓜組件。•Provider:提供包含store的context

connect

connect相當於一個容器組件的工廠。幫助我們創建了容器它的方法是cxonnect(mapStateToProps, mapDispatchToProps),connect是reactredux提供的一個方法,這個方法接收兩個參數mapStateToProps和mapDispatchToProps(當無計算時,為非必傳),執行結果依然是一個函數,所以才可以在後面又加一個圓括弧,把connect函數執行的結果立刻執行,這一次參數是Counter這個傻瓜組件。這裡有兩次函數執行,第一次是connect函數的執行,第二次是把connect函數返回的函數再次執行,最後產生的就是容器組件,mapStateToProps和mapDispatchToProps都可以包含第二個參數,代表ownProps,也就是直接傳遞給外層容器組件的props。

// 精簡後的ClickCounterimport React, { Component } from 'react';import PropTypes from 'prop-types';import Actions from '../Action/index';import { connect } from 'react-redux';    const styles = {  // ...}  // ownProps也就是直接傳遞給外層容器組件的props。// 把state轉化為屬性function mapStateToProps(state, ownProps) {    return {        count: state[ownProps.label]    }}// 定義改變邏輯,需dispatch觸發:function mapDispatchToProps(dispatch, ownProps) {    return {        onClickIncrementButton: () => {            dispatch(Actions.increment(ownProps.label));        },        onClickDecrementButton: () => {            dispatch(Actions.decrement(ownProps.label));        }    }}  function ClickCounter(props) {    const {        label,        count,        onClickDecrementButton        , onClickIncrementButton    } = props;      return (        <div style={styles.counter}>            <div style={styles.label}>{label}</div>            <button onClick={onClickDecrementButton}>-</button>            <div style={styles.showbox}>{count}</div>            <button onClick={onClickIncrementButton}>+</button>        </div>    )}  export default connect(mapStateToProps, mapDispatchToProps)(ClickCounter);

在AllCounter中,寫法也是大大簡化

import React from 'react'import {connect} from 'react-redux';  function AllCount({ sum }) {    return <div>Total Count: {sum}</div>}    function mapStateToProps(state, ownProps) {    let sum = 0;    Object.keys(state).forEach((key) => {        if (state.hasOwnProperty(key)) {            sum += state[key];        }    });    return { sum: sum };}  export default connect(mapStateToProps)(AllCount);

Provider

Provider的用法和之前定義的幾乎一致,而且不必再定義默認數據類型了:

  import React from 'react';  import ReactDOM from 'react-dom';  import './index.css';  import App from './view/App';  import * as serviceWorker from './serviceWorker';  // import Provider from './view/Provider'; 不再用自己造的輪子  import {Provider} from 'react-redux';  import store from './stores/index';      ReactDOM.render(<Provider store={store}>      <App />  </Provider>, document.getElementById('root'));    serviceWorker.unregister();

Redux是Flux框架的一個巨大改進,Redux強調單一數據源、保持狀態只讀和數據改變只能通過純函數完成的基本原則,和React的UI=render(state)思想完全契合。我們在這一章中用不同方法,循序漸進的改進了計數器,為的就是更清晰地理解每個改進背後的動因,最後,我們終於通過react-redux完成了React和Redux的融合。

Tags: