redux架構基礎
- 2020 年 1 月 2 日
- 筆記
。


本文書接 從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的融合。

