React 知识梳理(三):手写一个自己的 React-redux

上一次我们简单了解了一下 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>
 );
 }
}
复制代码

以上代码并不复杂,我们再来给他们写点样式,最后看下效果:

React 知识梳理(三):手写一个自己的 React-redux

我们看到,现在 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 已经被各个后代组件访问到:

React 知识梳理(三):手写一个自己的 React-redux 我们再来梳理下使用 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日

原文作者:吴永辉

本文来源掘金如需转载请紧急联系作者