react-redux
是 redux
官方 React
绑定库。它帮助咱们链接UI层和数据层。本文目的不是介绍 react-redux
的使用,而是要动手实现一个简易的 react-redux
,但愿可以对你有所帮助。javascript
首先思考一下,假若不使用 react-redux
,咱们的 react
项目中该如何结合 redux
进行开发呢。java
每一个须要与
redux
结合使用的组件,咱们都须要作如下几件事:react
store
中的状态store
中状态的改变,在状态改变时,刷新组件以下:git
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
中高阶组件能够实现逻辑的复用。github
文中所用到的 [Counter
代码] (github.com/YvetteLau/B…) 中的 myreact-redux/counter
中,建议先 clone
代码,固然啦,若是以为本文不错的话,给个star鼓励。redux
在 src
目录下新建一个 react-redux
文件夹,后续的文件都新建在此文件夹中。数组
文件建立在 react-redux/components
文件夹下:性能优化
咱们将重复的逻辑编写 connect
中。app
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
没法知道该绑定那些动做给你。ide
为此,咱们新增两个参数:mapStateToProps
和 mapDispatchToProps
,这两个参数负责告诉 connect
组件须要的 state
内容和将要派发的动做。
咱们知道 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>
)
}
}
复制代码
参考连接: