上一次我们简单了解了一下 redux(文章在这里),今天我们来结合 React,实现自己的 React-redux。
一、创建项目
我们用 create-react-app 创建一个新项目,删除 src 下的冗余部分,添加自己的文件,如下:
# 修改后的目录结构
++ src
++++ component
++++++ Head
-------- Head.js
++++++ Body
-------- Body.js
++++++ Button
-------- Button.js
---- App.js
---- index.css
---- index.js
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
// App.js
import React, { Component } from 'react';
import Head from './component/Head/Head';
import Body from './component/Body/Body';
export default class App extends Component {
render() {
return (
<div className="App">
<Head />
<Body />
</div>
);
}
}
# Head.js
import React, { Component } from 'react';
export default class Head extends Component {
render() {
return (
<div className="head">Head</div>
);
}
}
# Body.js
import React, { Component } from 'react';
import Button from '../Button/Button';
export default class Body extends Component {
render() {
return (
<div>
<div className="body">Body</div>
<Button />
</div>
);
}
}
# Button.js
import React, { Component } from 'react';
export default class Button extends Component {
render() {
return (
<div className="button">
<div className="btn">改变 head</div>
<div className="btn">改变 body</div>
</div>
);
}
}
复制代码
以上代码并不复杂,我们再来给他们写点样式,最后看下效果:
我们看到,现在 head ,和 body 内的文案都是我们写死的,这样并不利于我们的开发,因为这些值我们无法改变,现在我们想点击下边按钮的时候,改变相应的文案,以现在的代码我们是无法实现的。
当然,我们可以通过一系列 props 的传递,来达到我们的目的,可是,那样会相当繁琐,因为不仅涉及到父子组件的值传递,还有和兄弟组件的子组件之间的值传递。
此时,我们需要一个全局共享的 store ,让我们可以在任何地方都能轻松的访问,可以十分便捷的完成数据的获取和修改。
二、context
在 React 中,为我们提供了 context 这个 API 来解决这样的嵌套场景(context具体介绍在这里,在 React 16.3 以上的版本,context 已经有了更新,具体请看这里)。
context 为我们提供了一个全局共享的状态,在任何后代组件中,都可以很轻松的访问*组件的 store。
我们这样修改我们的代码:
# App.js
import PropTypes from 'prop-types';
...
export default class App extends Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
const state = {
head: '我是全局 head',
body: '我是全局 body',
headBtn: '修改 head',
bodyBtn: '修改 body'
}
return { store: state };
}
render() {
...
}
}
# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Head extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
this._upState();
}
_upState(){
const { store } = this.context;
this.setState({
...store
})
}
render() {
return (
<div className="head">{this.state.head}</div>
);
}
}
# body.js
import PropTypes from 'prop-types';
...
export default class Body extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
this._upState();
}
_upState(){
const { store } = this.context;
this.setState({
...store
})
}
render() {
return (
<div>
<div className="body">{this.state.body}</div>
<Button />
</div>
);
}
}
# Button.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Button extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
this._upState();
}
_upState(){
const { store } = this.context;
this.setState({
...store
})
}
render() {
return (
<div className="button">
<div className="btn">{this.state.headBtn}</div>
<div className="btn">{this.state.bodyBtn}</div>
</div>
);
}
}
复制代码
查看页面,我们可以看到,在顶层组件中的全局 store 已经被各个后代组件访问到:
我们再来梳理下使用 context 的步骤:1、在顶层组件中通过 childContextTypes 规定数据类型。
2、在顶层组件中通过 getChildContext 设置数据。
3、在后代组件中通过 contextTypes 规定数据类型。
4、在后代组件中通过 context 参数获取数据。
通过以上步骤,我们创建了一个全局共享的 store 。你可能会有疑问,为什么在后代组件中我们定义了 _upState 方法,而没有把内容直接写在生命周期中,这个问题先不回答,在下面,你将会看到为什么。现在,我们来把这个 store 和我们之前写的 redux 进行结合(有关 redux 的部分,请看上一篇文章,这里 。
三、React-redux
我们来新建 redux 文件夹,完成我们的 redux(关于以下代码含义,请看上一篇文章):
# index.js export * from './createStore';
export * from './storeChange';
# createStore.js export const createStore = (state, storeChange) => {
const listeners = [];
let store = state || {};
const subscribe = (listen) => listeners.push(listen);
const dispatch = (action) => {
const newStore = storeChange(store, action);
store = newStore;
listeners.forEach(item => item())
};
const getStore = () => {
return store;
}
return { store, dispatch, subscribe, getStore }
}
# storeChange.js export const storeChange = (store, action) => {
switch (action.type) {
case 'HEAD':
return {
...store,
head: action.head
}
case 'BODY':
return {
...store,
body: action.body
}
default:
return { ...store }
}
}
复制代码
通过以上代码,我们完成了 redux ,其中 createStore.js 的代码,几乎完全和上一篇内容相同,只是略作了修改,有兴趣的朋友可以自己看下。现在我们来和 context 结合:
# App.js
...
import { createStore, storeChange } from './redux';
export default class App extends Component {
static childContextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
getChildContext () {
const state = {
head: '我是全局 head',
body: '我是全局 body',
headBtn: '修改 head',
bodyBtn: '修改 body'
}
const { store, dispatch, subscribe, getStore } = createStore(state,storeChange)
return { store, dispatch, subscribe, getStore };
}
render() {
...
}
}
# Head.js
...
export default class Head extends Component {
static contextTypes = {
store: PropTypes.object,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
...
componentWillMount(){
const { subscribe } = this.context;
this._upState();
subscribe(() => this._upState())
}
_upState(){
const { getStore } = this.context;
this.setState({
...getStore()
})
}
render() {
...
}
}
# Body.js
...
export default class Body extends Component {
static contextTypes = {
// 和 Head.js 相同
}
...
componentWillMount(){
// 和 Head.js 相同
}
_upState(){
// 和 Head.js 相同
}
render() {
return (
<div>
<div className="body">{this.state.body}</div>
<Button />
</div>
);
}
}
# Button.js
...
export default class Button extends Component {
static contextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
// 和 Head.js 相同
}
_upState(){
// 和 Head.js 相同
}
render() {
...
}
}
复制代码
以上代码,我们用 createStore 方法,创建出全局的 store。并且把 store、 dispatch、subscribe 通过 context传递, 让各个后代组件可以轻易的获取到这些全局的属性。最后我们用 setState 来改变各个后代组件的 state ,并给 subscribe 中添加了监听函数,当 store 发生改变时,让组件重新获取到 store, 重新渲染。在这里,我们看到了 _upState 的用处,它让我们很方便的添加 store 改变后的回调。
观察页面,我们发现页面并没有异常,在后代页面依旧可以访问到 context。这样,是不是说明我们结合成功了呢?先别急,让我们来改变下数据试一下。我们修改 Button.js 给按键添加点击事件,来改变 store :
# Button.js
...
changeContext(type){
const { dispatch } = this.context;
dispatch({
type: type,
head: '我是修改后的数据'
});
}
render() {
return (
<div className="button">
<div className="btn" onClick={() => this.changeContext('HEAD')}>{this.state.headBtn}</div>
<div className="btn" onClick={() => this.changeContext('BODY')}>{this.state.bodyBtn}</div>
</div>
);
}
复制代码
点击按键,我们看到:数据成功刷新。 至此,我们已经成功的将自己的 redux 和 react 结合了起来。
四、优化
1、connect
虽然我们实现了 redux 和 react 的结合,但是我们看到,上面的代码是有很多问题的,比如:
1)有大量的重复逻辑
在各个后代组件中,我们都是在 context 中获取 store ,然后更新各自的 state ,还同样的添加了监听事件。
2)代码几乎不可复用
在各个后代组件中,对 context 的依赖过强。假设你的同事想用下 Body 组件,可是他的代码中并没有设置 context 那么 Body 组件就是不可用的。
关于这些问题,我们可以通过高阶组件来解决(关于高阶组件的问题,大家请点这里或者这里),我们可以把重复的代码逻辑,封装起来,我们给这个封装好的方法起个名字叫 connect 。 这只是一个名字而已,大家不必纠结,如果你愿意,你完全可以管它叫做 aaa。
我们在 redux 文件夹下新建一个 connect 文件:
# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
class Connect extends Component {
render(){
return (
<div className="connect">
<Comp />
</div>
);
}
}
return Connect;
}
复制代码
我们看到,connect 是一个高阶组件,它接收一个组件,然后返回处理后的组件。我们 Head 组件来验证一下这个高阶组件是否可用:
# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '../../redux';
class Head extends Component {
...
}
export default connect(Head);
复制代码
刷新页面我们可以知道,connect 正在发挥它应有的功能,已经成功的在 Head 组件外层套了一层 div:由此,我们是不是可以让 connect 做更多的事,比如,把有关 context 的东西都交给它,我们试着这样改造 connect 和 Head:
# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
const { subscribe } = this.context;
this._upState();
subscribe(() => this._upState())
}
_upState(){
const { getStore } = this.context;
this.setState({
...getStore()
})
}
render(){
return (
<div className="connect">
<Comp {...this.state} />
</div>
);
}
}
return Connect;
}
# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '../../redux';
class Head extends Component {
render() {
return (
<div className="head">{this.props.head}</div> // 从 props 中取值
);
}
}
export default connect(Head);
复制代码
我们看到,改造后的 Head 组件变得非常精简,我们只需要关心具体的业务逻辑,而任何于 context 有关的操作都被转移到了 connect 中去。我们按照同样的方式改造 Body 和 Button 组件:
# Body.js
...
class Body extends Component {
render() {
return (
<div>
<div className="body">{this.props.body}</div>
<Button />
</div>
);
}
}
export default connect(Body)
# Button.js
...
class Button extends Component {
changeContext(type, value){
const { dispatch } = this.context; // context 已经不存在了
dispatch({
type: type,
head: value
});
}
render() {
return (
<div className="button">
<div className="btn" onClick={() => this.changeContext('HEAD', '我是改变的数据1')}>{this.props.headBtn}</div>
<div className="btn" onClick={() => this.changeContext('HEAD', '我是改变的数据2')}>{this.props.bodyBtn}</div>
</div>
);
}
}
export default connect(Button)
复制代码
刷新页面,并没有什么问题,一切似乎都很美好,可是当我们点击按键时,错误降临。 我们发现,在 Button 中,dispatch 是无法获取到的,我们现在唯一的数据来源都是通过 props ,而在 connect 中,我们并没有处理 dispatch ,那么,我们继续改造我们的 connect:
# Button.js
...
const { dispatch } = this.props; // 从 props 中取值
...
# connect.js
...
export const connect = (Comp) => {
class Connect extends Component {
...
constructor (props) {
super(props)
this.state = {
dispatch: () => {}
};
}
componentWillMount(){
const { subscribe, dispatch } = this.context; // 取出 dispatch
this.setState({
dispatch
})
this._upState();
subscribe(() => this._upState())
}
...
}
return Connect;
}
复制代码
现在看来,一切似乎都已经解决。让我们再来一起回顾下我们究竟做了什么:
1)我们封装了 connect ,把所有有关的 connect 的操作都交给他来负责。
2)我们改造了后代组件,让它们从 props 中来获取数据,不再依赖 context。
现在,再来对照之前我们提出的问题,发现,我们已经很好的解决了它们。
可是,这样真的就可以了吗?
我们再来观察 connect 中的代码,我们发现,所有的 PropTypes 都是我们固定写死的,缺乏灵活性,也不太利于我们开发,毕竟,每个组件所要获取的数据都不尽相同,如果能让 connect 再接收一个参数,来规定 PropTypes 那再好不过了。
根据这个需求,我们来继续改造我们的代码:
# connect.js
...
export const connect = (Comp, propsType) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func,
...propsType
}
...
}
return Connect;
}
# Head.js
...
const propsType = {
store: PropTypes.object,
}
export default connect(Head, propsType);
复制代码
以上,我们重新改造了 connect ,让他接收两个参数,把一些固定要传递的属性,我们可以写死,然后再添加进我们在每个组件内部单独定义的 propsType。
2、Provider
我们看到,在所有的后代组件中,已经分离出了有关 context 的操作,但是,在 App.js 中,依旧还有和 context 相关的内容。其实,在 App 中用到 context 只是为了把 store 存放进去,好让后代组件可以从中获取数据。那么,我们完全可以通过容器组件来进行状态提升,把这部分脏活从 App 组件中分离出来,提升到新建的容器组件中。我们只需要给他传入需要存放进 context 的 store 就可以了。
依据之前的想法,我们在 redux 文件夹下新建一个 Provider,并把所有和业务无关的代码从 App 中取出:
# Provider
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { createStore, storeChange } from '../redux';
export class Provider extends Component {
static childContextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
getChildContext () {
const state = this.props.store;
const { store, dispatch, subscribe, getStore } = createStore(state,storeChange)
return { store, dispatch, subscribe, getStore };
}
render(){
return (
<div className="provider">{this.props.children}</div>
);
}
}
# App.js
...
export default class App extends Component {
render() {
return (
<div className="App">
<Head />
<Body />
</div>
);
}
}
# index.js
...
import { Provider } from './redux'
const state = {
head: '我是全局 head',
body: '我是全局 body',
headBtn: '修改 head',
bodyBtn: '修改 body'
}
ReactDOM.render(
<Provider store={state}>
<App />
</Provider>,
document.getElementById('root')
);
复制代码
经过改造的 App 组件也变得非常清爽。
我们在 index.js 中定义了全局 store ,通过容器组件 Provider 塞入 context 中,让所有的后代组件都可以轻松获取到,而在 App 组件中,我们只需要关注具体的业务逻辑就好。
最后的话
本文通过一些简单的代码示例,完成了一个自己的 react-redux ,当然,以上代码还过于简陋,存在很多问题,和我们常用的 react-redux 库也有些许区别,我们重点在于了解它们内部的一些原理。
如有描述不正确的地方,欢迎大家指正
原文发布时间:06月21日
原文作者:吴永辉
本文来源掘金如需转载请紧急联系作者