本文会通过一个简单的例子讲解react-redux的使用及其原理,并且该例子会用redux和react-redux都写一遍,对比其不同来加深理解。
我们要实现的例子很简单,就是页面上有一个按钮和一个数字,点击add按钮的时候,数字会加1。虽然是个极简的例子,但已经包括了数据的响应化和事件交互这两个最核心的功能。
实现的例子页面截图如下:
redux
首先我们来看下redux是如何实现该功能的(redux的原理及实现可以看上篇文章):
首先通过redux提供的createStore
创建一个store实例,createStore
接收一个reducer
函数作为参数。代码实现如下:
// store.js
import {createStore} from 'redux';
function countReducer(state=0, action) {
if (action.type === 'ADD') {
return state + 1;
}
return state;
}
const store = createStore(countReducer);
export default store;
store提供了三个方法供外部调用,分别是dispatch、subscribe和getState。
- dispatch:通过调用
dispatch(action)
告诉store要执行什么行为,store会调用reducer函数执行具体的行为更新state。以上面的例子举例,当我们调用store.dispatch({action: 'ADD'})
,那么store就会调用countReducer
函数执行,返回的值为新的state,那么现在的state就+1了。 - subscribe:该方法提供的是订阅功能,其参数是一个函数,其订阅的是state变化这个行为,就是说每当state变化的时候,就会执行订阅的函数。例如当我们调用
store.subscribe(fn)
;那么当store里的state变化的时候,fn就会被执行。 - getState:获取当前的state,我们可以通过调用store.state()来获取当前最新的state。
小心得:dispatch和subscribe其实可以理解为就是一种发布订阅模式。
当我们写好store后,就可以写我们的业务组件了,代码如下:
import React, {Component} from 'react';
import store from '../store'; // 引入store
export default class ReactReduxPage extends Component {
componentDidMount () {
// 在组件挂在的时候订阅state的变化
store.subscribe(() => {
this.forceUpdate(); // 每当state变化的时候就重新渲染组件
})
}
// 当点击按钮的时候,dispatch一个ADD action,触发store里的reducer方法更新state
add () {
store.dispatch({
type: 'ADD'
})
}
render () {
return (
<div>
<div>number: {store.getState()}</div>
<button onClick={this.add}>add</button>
</div>
)
}
}
以上就是用redux实现该例子的所有代码,使用redux我们需要自己调用store.subscribe、store.dispatch和store.getState这些方法,并且在state更新的时候,要自己重新触发render,业务逻辑复杂的话还是会有一点麻烦,所以redux的作者封装了一个专门给react使用的redux模块,就是react-redux。
react-redux
react-redux其实就是对redux进行了再一步的封装,实际底层还是使用的redux。我们先用react-redux来实现本文的小例子来看下react-redux是怎么用的。然后再通过自己实现一个react-redux框架来看下redux-react是怎么对redux进行再一步的封装的。
react-redux实现计数器
我们用react-redux实现和上面一样的例子:
Provider
react-redux提供了一个Provider
组件,该组件可以将store提供给所有的子组件使用,其利用的是react的context属性,具体使用方法如下:
// 入口文件index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// ========= 重点 =========
import {Provider} from "react-redux";
import store from './store';
// ========= 重点 =========
ReactDOM.render(
// 在App组件外包一层Provider
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
如上重点就是我们在入口文件index.js引入Provider
和store
;然后利用Provider
将store
注入到所有的子组件中,那么所有的子组件就都可以拿到store了。该特性利用的是react的context属性,对context不了解的可以去看下react文档,这里不再赘述。
store
store.js的代码和上面的redux是一样的,这里就不重复写一遍了。
connect
然后我们开始写我们的业务组件,我们的业务组件现在变成了如下所示:
import React, {Component} from 'react';
import {connect} from 'react-redux';
// 重点!使用了connect高阶组件
// connect使用方式为 connect(mapStateToProps, mapDispatchToProps)(originalComponent)
// 执行 connect(mapStateToProps, mapDispatchToProps)(originalComponent)后会返回一个新的组件
export default connect(
state => ({count: state}),
dispatch => ({add: () => dispatch({type: 'ADD'})})
)(
class ReactReduxPage extends Component {
render () {
const {count, add} = this.props
console.error('this.props:', this.props);
return (
<div>
<div>number: {count}</div>
<button onClick={add}>add</button>
</div>
)
}
}
)
如上所示,我们使用react-redux写组件的时候,组件的属性和方法都要通过props获取。例如上面例子的this.props.count
以及this.props.add
。props里的属性和方法是哪里来的呢?答案是connect函数放进去的,connect接收两个函数作为参数,分别是mapStateToProps和mapDispatchToProps,以上面的例子为例分别对应如下:
mapStateToProps函数: state => ({count: state})
mapDispatchToProps函数: dispatch => ({add: () => dispatch({type: 'ADD'})})
这两个函数都会返回一个对象,对象里面的键值对都会作为props里的键值对,其原理如下:
mapStateToProps函数执行返回 obj1 : {count: state}
mapDispatchToProps函数执行返回 obj2 :{add: () => dispatch({type: 'ADD'})}
那么最后提供给组件使用的props就是:
{
...obj1,
...obj2
}
也就是
{
count: state,
add: () => dispatch({type: 'ADD'}
}
当我们点击add按钮的时候,就会调用this.props.add
方法,add方法会调用dispatch({type: 'ADD'})
,然后就会执行store里的reducer方法更新state,更新state后react-redux就会重新帮我们渲染组件,下面我们通过自己实现react-redux来看下这个流程是怎么运行的。
react-redux 源码实现
从上面的代码中我们可以看到,react-redux给外面提供了两个方法,分别是Provider
和connect
,所以react-redux内的代码结构应该是这样的:
// react-redux.js
import React, {Component} from 'react';
export const connect = (
mapStateToProps,
mapDispatchToProps
) => WrappedComponent => {
// 执行函数体内代码逻辑
// return 一个组件
}
export class Provider extends Component {
render () {
// return ...
}
}
Provider源码实现
Provider的作用是向下提供store,让组件树内的所有组件都能获取到store实例。我们先回顾一下主入口文件index.js是如何使用Provider向下提供store的:
// 入口文件index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// ========= 重点 =========
import {Provider} from "react-redux";
import store from './store';
// ========= 重点 =========
ReactDOM.render(
// 在App组件外包一层Provider
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
然后我们再来自己实现react-redux里的Provider:
// react-redux.js
import React, {Component} from 'react';
// 创建一个上下文,名字随便取
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
const StoreContext = React.createContext();
// 重点!Provider的实现
export class Provider extends Component {
render () {
// 使用一个 Provider 来将当前的 store 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
return (
<StoreContext.Provider value={this.props.store}>
{this.props.children}
</StoreContext.Provider>
)
}
}
从上面的代码可以看到,Provider是使用react的context来实现的,context的作用可以查看官方文档:
react.docschina.org/docs/contex…
我们通过创建一个上下文StoreContext
,然后用StoreContext.Provider
向组件树向下传递store。组件树里的组件通过static contextType = StoreContext;
,组件内的this.context
就指向StoreContext.Provider
提供的value里的值了,也就是store实例。context就不再过多赘述了,看下官方文档的用法,上面的代码自然就能理解了。
connect源码实现
connect 是一个双箭头函数,最终返回的是一个组件。
首先执行 connect(mapStateToProps,mapDispatchToProps)
返回一个函数,然后该函数再接收一个组件作为参数,最后返回一个新的组件,这是高阶组件的用法。
所以我们使用connect的方式是这样的connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
,WrappedComponent是一个纯组件,它只接收props,通过props来渲染出组件的内容,内部不保存状态。props通过mapStateToProps
和mapDispatchToProps
这两个函数提供。react-redux这么设计的原因就是为了让WrappedComponent只负责接收props数据和渲染组件,不用关心状态,给它什么就显示什么,所以WrappedComponent
组件的逻辑会简单清晰一些,并且组件的职责也更加明确。而负责数据管理和逻辑的这些操作都放在执行 connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
后返回的这个组件里完成,这里会比较绕,后面会通过写代码详细讲解。
当我们执行connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
的时候,函数体内就可以使用mapStateToProps
、mapDispatchToProps
、WrappedComponent
这几个参数了。connect函数的作用就是返回一个新的封装过的组件,而封装这个组件时候需要用到mapStateToProps
、mapDispatchToProps
、WrappedComponent
这几个参数来处理一些逻辑。
tips小思考:connect不用双箭头函数可不可以呢?也是可以的,因为connet函数最终是要返回一个新的组件,而在封装这个新的组件的过程中需要用到mapStateToProps
、mapDispatchToProps
、WrappedComponent
来做一些事情,不用双箭头也可以达到这个效果,我们像下面这样写也能实现一样的功能:
export const connect = (
mapStateToProps,
mapDispatchToProps,
WrappedComponent
) => {
// 执行函数体内代码逻辑
// return 一个组件
}
然后使用的时候用
connect(
mapStateToProps,
mapDispatchToProps,
WrappedComponent
)
不需要使用
connect(
mapStateToProps,
mapDispatchToProps
)(WrappedComponent)
至于react-redux官方为什么用双箭头呢,我觉得主要是有以下几个优点:
- 1、参数职责划分明确,第一个箭头函数的参数就是用来映射props的,第二个箭头函数的参数就是用来传要被封装的组件的
- 2、逼格比较高,够骚气,充分利用了闭包和函数作用域的原理
我们先实现一个能渲染出WrappedComponent组件内容的版本,主要看标注了重点的部分,因为connect返回的组件在Provider内部,所以可以拿到store实例,所以就能调用store的getState方法拿到最新state。然后我们调用mapStateToProps(state)
,就能得到要传递给WrappedComponent的stateProps了。
export const connect = (
mapStateToProps,
mapDispatchToProps
) => WrappedComponent => {
return class extends Component {
// 指定 contextType 读取当前的 store context。
// React 会往上找到最近的 store Provider,然后使用它的值。
static contextType = StoreContext;
constructor(props) {
super(props);
this.state = {
props: {}
};
}
// ============== 重点 ==============
componentDidMount () {
this.update();
}
update () {
// this.context已经指向我们的store实例了
const {dispatch, subscribe, getState} = this.context;
// 调用mapStateToProps,并将store最新的state作为参数
// 返回我们要传递给WrappedComponent的props
let stateProps = mapStateToProps(getState());
// 调用setState触发render,更新WrappedComponent内容
this.setState({
props: {
...stateProps
}
})
}
render() {
return <WrappedComponent {...this.state.props}/>
}
// ============== 重点 ==============
}
}
上面的代码已经能显示我们的初始化界面了,但是现在还不能处理事件,所以我们需要使用mapDispatchToProps
生成事件到props的映射,再来回顾下这两个函数:
mapStateToProps函数: state => ({count: state})
mapDispatchToProps函数: dispatch => ({add: () => dispatch({type: 'ADD'})})
增加了对事件处理的代码如下,新增的代码就两行,就是调用mapDispatchToProps
生成事件相关的props传递给WrappedComponent组件。
update () {
// this.context已经指向我们的store实例了
const {dispatch, getState} = this.context;
// 调用mapStateToProps,并将store最新的state作为参数
// 返回我们要传递给WrappedComponent的props
let stateProps = mapStateToProps(getState());
// 调用mapDispatchToProps返回事件相关的props
let dispatchProps = mapDispatchToProps(dispatch); // !重点新加代码
// 调用setState触发render,更新WrappedComponent内容
this.setState({
props: {
...stateProps,
...dispatchProps // !重点新加代码
}
})
}
state变化的时候需要重新渲染组件,所以我们还需要增加一个订阅功能:
componentDidMount () {
this.update();
// ===== 新加代码 =====
const {subscribe} = this.context;
// state变化的时候重新渲染组件
subscribe (() => {
this.update()
})
// ===== 新加代码 =====
}
自己实现的react-redux完整代码:
// react-redux简易源码
import React, {Component} from 'react';
// 创建一个上下文,名字随便取
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
const StoreContext = React.createContext();
export const connect = (
mapStateToProps,
mapDispatchToProps
) => WrappedComponent => {
return class extends Component {
// 指定 contextType 读取当前的 store context。
// React 会往上找到最近的 store Provider,然后使用它的值。
static contextType = StoreContext;
constructor(props) {
super(props);
this.state = {
props: {}
};
}
// ============== 重点 ==============
componentDidMount () {
this.update();
const {subscribe} = this.context;
// state变化的时候重新渲染组件
subscribe (() => {
this.update()
})
}
update () {
// this.context已经指向我们的store实例了
const {dispatch, getState} = this.context;
// 调用mapStateToProps,并将store最新的state作为参数
// 返回我们要传递给WrappedComponent的props
let stateProps = mapStateToProps(getState());
// 调用mapDispatchToProps返回事件相关的props
let dispatchProps = mapDispatchToProps(dispatch); // !重点新加代码
// 调用setState触发render,更新WrappedComponent内容
this.setState({
props: {
...stateProps,
...dispatchProps // !重点新加代码
}
})
}
render() {
return <WrappedComponent {...this.state.props}/>
}
// ============== 重点 ==============
}
}
export class Provider extends Component {
render () {
// 使用一个 Provider 来将当前的 store 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
return (
<StoreContext.Provider value={this.props.store}>
{this.props.children}
</StoreContext.Provider>
)
}
}
另:mapDispatchToProps也可以为对象,这里为了演示方便就不再讲解了,只介绍为funtion的情况
业务组件的代码也贴一下
// 业务组件代码
import React, {Component} from 'react';
import {connect} from 'react-redux';
// 重点!使用了connect高阶组件
// connect使用方式为 connect(mapStateToProps, mapDispatchToProps)(originalComponent)
// 执行 connect(mapStateToProps, mapDispatchToProps)(originalComponent)后会返回一个新的组件
export default connect(
state => ({count: state}),
dispatch => ({add: () => dispatch({type: 'ADD'})})
)(
class ReactReduxPage extends Component {
render () {
const {count, add} = this.props
console.error('this.props:', this.props);
return (
<div>
<div>number: {count}</div>
<button onClick={add}>add</button>
</div>
)
}
}
)
入口js代码:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// ========= 重点 =========
import {Provider} from "react-redux";
import store from './store';
// ========= 重点 =========
ReactDOM.render(
// 在App组件外包一层Provider
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
以上就是react-redux源码的解析。