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> ) } }
参考连接: