react-redux
是 redux
官方 React
綁定庫。它幫助咱們鏈接UI層和數據層。本文目的不是介紹 react-redux
的使用,而是要動手實現一個簡易的 react-redux
,但願可以對你有所幫助。javascript
首先思考一下,假若不使用 react-redux
,咱們的 react
項目中該如何結合 redux
進行開發呢。java
每一個須要與
redux
結合使用的組件,咱們都須要作如下幾件事:
store
中的狀態store
中狀態的改變,在狀態改變時,刷新組件以下:react
import React from 'react'; import store from '../store'; import actions from '../store/actions/counter'; /** * reducer 是 combineReducer({counter, ...}) * state 的結構爲 * { * counter: {number: 0}, * .... * } */ class Counter extends React.Component { constructor(props) { super(props); this.state = { number: store.getState().counter.number } } componentDidMount() { this.unsub = store.subscribe(() => { if(this.state.number === store.getState().counter.number) { return; } this.setState({ number: store.getState().counter.number }); }); } render() { return ( <div> <p>{`number: ${this.state.number}`}</p> <button onClick={() => {store.dispatch(actions.add(2))}}>+</button> <button onClick={() => {store.dispatch(actions.minus(2))}}>-</button> <div> ) } componentWillUnmount() { this.unsub(); } }
若是咱們的項目中有不少組件須要與 redux
結合使用,那麼這些組件都須要重複寫這些邏輯。顯然,咱們須要想辦法複用這部分的邏輯,否則會顯得咱們很蠢。咱們知道,react
中高階組件能夠實現邏輯的複用。git
文中所用到的 Counter
代碼在 https://github.com/YvetteLau/Blog
中的 myreact-redux/counter
中,建議先 clone
代碼,固然啦,若是以爲本文不錯的話,給個star鼓勵。github
在 src
目錄下新建一個 react-redux
文件夾,後續的文件都新建在此文件夾中。redux
文件建立在 react-redux/components
文件夾下:數組
咱們將重複的邏輯編寫 connect
中。性能優化
import React, { Component } from 'react'; import store from '../../store'; export default function connect (WrappedComponent) { return class Connect extends Component { constructor(props) { super(props); this.state = store.getState(); } componentDidMount() { this.unsub = store.subscribe(() => { this.setState({ this.setState(store.getState()); }); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {...this.state} {...this.props}/> ) } } }
有個小小的問題,儘管這邏輯是重複的,可是每一個組件須要的數據是不同的,不該該把全部的狀態都傳遞給組件,所以咱們但願在調用 connect
時,可以將須要的狀態內容告知 connect
。另外,組件中可能還須要修改狀態,那麼也要告訴 connect
,它須要派發哪些動做,不然 connect
沒法知道該綁定那些動做給你。app
爲此,咱們新增兩個參數:mapStateToProps
和 mapDispatchToProps
,這兩個參數負責告訴 connect
組件須要的 state
內容和將要派發的動做。ide
咱們知道 mapStateToProps
和 mapDispatchToProps
的做用是什麼,可是目前爲止,咱們還不清楚,這兩個參數應該是一個什麼樣的格式傳遞給 connect
去使用。
import { connect } from 'react-redux'; .... //connect 的使用 export default connect(mapStateToProps, mapDispatchToProps)(Counter);
mapStateToProps 告訴 connect
,組件須要綁定的狀態。
mapStateToProps
須要從整個狀態中挑選組件須要的狀態,可是在調用 connect
時,咱們並不能獲取到 store
,不過 connect
內部是能夠獲取到 store
的,爲此,咱們將 mapStateToProps
定義爲一個函數,在 connect
內部調用它,將 store
中的 state
傳遞給它,而後將函數返回的結果做爲屬性傳遞給組件。組件中經過 this.props.XXX
來獲取。所以,mapStateToProps
的格式應該相似下面這樣:
//將 store.getState() 傳遞給 mapStateToProps mapStateToProps = state => ({ number: state.counter.number });
mapDispatchToProps 告訴 connect
,組件須要綁定的動做。
回想一下,組件中派發動做:store.dispatch({actions.add(2)})
。connect
包裝以後,咱們仍要能派發動做,確定是 this.props.XXX()
這樣的一種格式。
好比,計數器的增長,調用 this.props.add(2)
,就是須要派發 store.dispatch({actions.add(2)})
,所以 add
屬性,對應的內容就是 (num) => { store.dispatch({actions.add(num)}) }
。傳遞給組件的屬性相似下面這樣:
{ add: (num) => { store.dispatch(actions.add(num)) }, minus: (num) => { store.dispatch(actions.minus(num)) } }
和 mapStateToProps
同樣,在調用 connect
時,咱們並不能獲取到 store.dispatch
,所以咱們也須要將 mapDispatchToProps
設計爲一個函數,在 connect
內部調用,這樣能夠將 store.dispatch
傳遞給它。因此,mapStateToProps
應該是下面這樣的格式:
//將 store.dispacth 傳遞給 mapDispatchToProps mapDispatchToProps = (dispatch) => ({ add: (num) => { dispatch(actions.add(num)) }, minus: (num) => { dispatch(actions.minus(num)) } })
至此,咱們已經搞清楚 mapStateToProps
和 mapDispatchToProps
的格式,是時候進一步改進 connect
了。
connect 1.0 版本
import React, { Component } from 'react'; import store from '../../store'; export default function connect (mapStateToProps, mapDispatchToProps) { return function wrapWithConnect (WrappedComponent) { return class Connect extends Component { constructor(props) { super(props); this.state = mapStateToProps(store.getState()); this.mappedDispatch = mapDispatchToProps(store.dispatch); } componentDidMount() { this.unsub = store.subscribe(() => { const mappedState = mapStateToProps(store.getState()); //TODO 作一層淺比較,若是狀態沒有改變,則不setState this.setState(mappedState); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } }
咱們知道,connect
是做爲 react-redux
庫的方法提供的,所以咱們不可能直接在 connect.js
中去導入 store
,這個 store
應該由使用 react-redux
的應用傳入。react
中數據傳遞有兩種:經過屬性 props
或者是經過上下文對象 context
,經過 connect
包裝的組件在應用中分佈,而 context
設計目的是爲了共享那些對於一個組件樹而言是「全局」的數據。
咱們須要把 store
放在 context
上,這樣根組件下的全部子孫組件均可以獲取到 store
。這部份內容,咱們固然能夠本身在應用中編寫相應代碼,不過很顯然,這些代碼在每一個應用中都是重複的。所以咱們把這部份內容也封裝在 react-redux
內部。
此處,咱們使用舊的 Context API
來寫(鑑於咱們實現的 react-redux 4.x 分支的代碼,所以咱們使用舊版的 context API)。
咱們須要提供一個 Provider
組件,它的功能就是接收應用傳遞過來的 store
,將其掛在 context
上,這樣它的子孫組件就均可以經過上下文對象獲取到 store
。
文件建立在 react-redux/components
文件夾下:
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class Provider extends Component { static childContextTypes = { store: PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, getState: PropTypes.func.isRequired }).isRequired } constructor(props) { super(props); this.store = props.store; } getChildContext() { return { store: this.store } } render() { /** * 早前返回的是 return Children.only(this.props.children) * 致使Provider只能包裹一個子組件,後來取消了此限制 * 所以此處,咱們直接返回 this.props.children */ return this.props.children } }
文件建立在 react-redux
目錄下:
此文件只作一件事,即將 connect
和 Provider
導出
import connect from './components/connect'; import Provider from './components/Provider'; export { connect, Provider }
使用時,咱們只須要引入 Provider
,將 store
傳遞給 Provider
。
import React, { Component } from 'react'; import { Provider } from '../react-redux'; import store from './store'; import Counter from './Counter'; export default class App extends Component { render() { return ( <Provider store={store}> <Counter /> </Provider> ) } }
至此,Provider
的源碼和使用已經說明清楚了,不過相應的 connect
也須要作一些修改,爲了通用性,咱們須要從 context
上去獲取 store
,取代以前的導入。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default function connect(mapStateToProps, mapDispatchToProps) { return function wrapWithConnect(WrappedComponent) { return class Connect extends Component { //PropTypes.shape 這部分代碼與 Provider 中重複,所以後面咱們能夠提取出來 static contextTypes = { store: PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, getState: PropTypes.func.isRequired }).isRequired } constructor(props, context) { super(props, context); this.store = context.store; //源碼中是將 store.getState() 給了 this.state this.state = mapStateToProps(this.store.getState()); this.mappedDispatch = mapDispatchToProps(this.store.dispatch); } componentDidMount() { this.unsub = this.store.subscribe(() => { const mappedState = mapStateToProps(this.store.getState()); //TODO 作一層淺比較,若是狀態沒有改變,則無需 setState this.setState(mappedState); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } }
使用 connect
關聯 Counter
與 store
中的數據。
import React, { Component } from 'react'; import { connect } from '../react-redux'; import actions from '../store/actions/counter'; class Counter extends Component { render() { return ( <div> <p>{`number: ${this.props.number}`}</p> <button onClick={() => { this.props.add(2) }}>+</button> <button onClick={() => { this.props.minus(2) }}>-</button> </div> ) } } const mapStateToProps = state => ({ number: state.counter.number }); const mapDispatchToProps = (dispatch) => ({ add: (num) => { dispatch(actions.add(num)) }, minus: (num) => { dispatch(actions.minus(num)) } }); export default connect(mapStateToProps, mapDispatchToProps)(Counter);
store/actions/counter.js 定義以下:
import { INCREMENT, DECREMENT } from '../action-types'; const counter = { add(number) { return { type: INCREMENT, number } }, minus(number) { return { type: DECREMENT, number } } } export default counter;
至此,咱們的 react-redux
庫已經可使用了,不過頗有不少細節問題待處理:
mapDispatchToProps
的定義寫起來有點麻煩,不夠簡潔redux
中的 bindActionCreators
,藉助於此方法,咱們能夠容許傳遞 actionCreator
給 connect
,而後在 connect
內部進行轉換。connect
和 Provider
中的 store
的 PropType
規則能夠提取出來,避免代碼的冗餘mapStateToProps
和 mapDispatchToProps
能夠提供默認值mapStateToProps
默認值爲 state => ({})
; 不關聯 state
;mapDispatchToProps
的默認值爲 dispatch => ({dispatch})
,將 store.dispatch
方法做爲屬性傳遞給被包裝的屬性。
store.getState()
給 mapStateToProps
,可是極可能在篩選過濾須要的 state
時,須要依據組件自身的屬性進行處理,所以,能夠將組件自身的屬性也傳遞給 mapStateToProps
,一樣的緣由,也將自身屬性傳遞給 mapDispatchToProps
。咱們將 store
的 PropType 規則提取出來,放在 utils/storeShape.js
文件中。
淺比較的代碼放在 utils/shallowEqual.js
文件中,通用的淺比較函數,此處不列出,有興趣能夠直接閱讀下代碼。
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import storeShape from '../utils/storeShape'; import shallowEqual from '../utils/shallowEqual'; /** * mapStateToProps 默認不關聯state * mapDispatchToProps 默認值爲 dispatch => ({dispatch}),將 `store.dispatch` 方法做爲屬性傳遞給組件 */ const defaultMapStateToProps = state => ({}); const defaultMapDispatchToProps = dispatch => ({ dispatch }); export default function connect(mapStateToProps, mapDispatchToProps) { if(!mapStateToProps) { mapStateToProps = defaultMapStateToProps; } if (!mapDispatchToProps) { //當 mapDispatchToProps 爲 null/undefined/false...時,使用默認值 mapDispatchToProps = defaultMapDispatchToProps; } return function wrapWithConnect(WrappedComponent) { return class Connect extends Component { static contextTypes = { store: storeShape }; constructor(props, context) { super(props, context); this.store = context.store; //源碼中是將 store.getState() 給了 this.state this.state = mapStateToProps(this.store.getState(), this.props); if (typeof mapDispatchToProps === 'function') { this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props); } else { //傳遞了一個 actionCreator 對象過來 this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch); } } componentDidMount() { this.unsub = this.store.subscribe(() => { const mappedState = mapStateToProps(this.store.getState(), this.props); if (shallowEqual(this.state, mappedState)) { return; } this.setState(mappedState); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } }
如今,咱們的 connect
容許 mapDispatchToProps
是一個函數或者是 actionCreators
對象,在 mapStateToProps
和 mapDispatchToProps
缺省或者是 null
時,也能表現良好。
不過還有一個問題,connect
返回的全部組件名都是 Connect
,不便於調試。所以咱們能夠爲其新增 displayName
。
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import storeShape from '../utils/storeShape'; import shallowEqual from '../utils/shallowEqual'; /** * mapStateToProps 缺省時,不關聯state * mapDispatchToProps 缺省時,設置其默認值爲 dispatch => ({dispatch}),將`store.dispatch` 方法做爲屬性傳遞給組件 */ const defaultMapStateToProps = state => ({}); const defaultMapDispatchToProps = dispatch => ({ dispatch }); function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; } export default function connect(mapStateToProps, mapDispatchToProps) { if(!mapStateToProps) { mapStateToProps = defaultMapStateToProps; } if(!mapDispatchToProps) { //當 mapDispatchToProps 爲 null/undefined/false...時,使用默認值 mapDispatchToProps = defaultMapDispatchToProps; } return function wrapWithConnect (WrappedComponent) { return class Connect extends Component { static contextTypes = storeShape; static displayName = `Connect(${getDisplayName(WrappedComponent)})`; constructor(props) { super(props); //源碼中是將 store.getState() 給了 this.state this.state = mapStateToProps(store.getState(), this.props); if(typeof mapDispatchToProps === 'function') { this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props); }else{ //傳遞了一個 actionCreator 對象過來 this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch); } } componentDidMount() { this.unsub = store.subscribe(() => { const mappedState = mapStateToProps(store.getState(), this.props); if(shallowEqual(this.state, mappedState)) { return; } this.setState(mappedState); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } }
至此,react-redux
咱們就基本實現了,不過這個代碼並不完善,好比,ref
丟失的問題,組件的 props
變化時,從新計算 this.state
和 this.mappedDispatch
,沒有進一步進行性能優化等。你能夠在此基礎上進一步進行處理。
react-redux
主幹分支的代碼已經使用 hooks
改寫,後期若是有時間,會輸出一篇新版本的代碼解析。
最後,使用咱們本身編寫的 react-redux
和 redux
編寫了 Todo
的demo,功能正常,代碼在 在 https://github.com/YvetteLau/Blog
中的 myreact-redux/todo
下。
附上新老 context API
的使用方法:
目前有兩個版本的 context API
,舊的 API 將會在全部 16.x 版本中獲得支持,可是將來版本中會被移除。
const MyContext = React.createContext(defaultValue);
建立一個 Context
對象。當 React
渲染一個訂閱了這個 Context
對象的組件,這個組件會從組件樹中離自身最近的那個匹配的 Provider
中讀取到當前的 context
值。
注意:只有當組件所處的樹中沒有匹配到 Provider
時,其 defaultValue
參數纔會生效。
首先建立 Context 對象
import React from 'react'; const MyContext = React.createContext(null); export default MyContext;
<MyContext.Provider>
的 value
中(即 context 值)<MyContext.Provider>
包裹import React from 'react'; import MyContext from './Context'; import Content from './Content'; class Pannel extends React.Component { state = { theme: { color: 'rgb(0, 51, 254)' } } render() { return ( // 屬性名必須叫 value <MyContext.Provider value={this.state.theme}> <Content /> </MyContext.Provider> ) } }
類組件
Class.contextType
: static contextType = ThemeContext
;this.context
獲取 <ThemeContext.Provider>
中 value
的內容(即 context
值)//類組件 import React from 'react'; import ThemeContext from './Context'; class Content extends React.Component { //定義了 contextType 以後,就能夠經過 this.context 獲取 ThemeContext.Provider value 中的內容 static contextType = ThemeContext; render() { return ( <div style={{color: `2px solid ${this.context.color}`}}> //.... </div> ) } }
函數組件
<ThemeContext.Consumer>
中<ThemeContext.Consumer>
的子元素是一個函數,入參 context
值(Provider
提供的 value
)。此處是 {color: XXX}
import React from 'react'; import ThemeContext from './Context'; export default function Content() { return ( <ThemeContext.Consumer> { context => ( <div style={{color: `2px solid ${context.color}`}}> //.... </div> ) } </ThemeContext.Consumer> ) }
childContextTypes
(驗證 getChildContext
返回的類型)getChildContext
方法import React from 'react'; import PropTypes from 'prop-types'; import Content from './Content'; class Pannel extends React.Component { static childContextTypes = { theme: PropTypes.object } getChildContext() { return { theme: this.state.theme } } state = { theme: { color: 'rgb(0, 51, 254)' } } render() { return ( // 屬性名必須叫 value <> <Content /> </> ) } }
contextTypes
(聲明和驗證須要獲取的狀態的類型)import React from 'react'; import PropTypes from 'prop-types'; class Content extends React.Component { static contextTypes = PropTypes.object; render() { return ( <div style={{color: `2px solid ${this.context.color}`}}> //.... </div> ) } }
參考連接: