上一次我们简单了解了一下 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.jsimport React from 'react';import ReactDOM from 'react-dom';import './index.css';import App from './App';ReactDOM.render(<App />, document.getElementById('root'));// App.jsimport 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.jsimport React, { Component } from 'react';export default class Head extends Component { render() { return ( <div className="head">Head</div> ); }}# Body.jsimport 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.jsimport 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.jsimport PropTypes from 'prop-types';...export default class App extends Component { static childContextTypes = { store: PropTypes. } getChildContext () { const state = { head: '我是全局 head', body: '我是全局 body', headBtn: '修改 head', bodyBtn: '修改 body' } return { store: state }; } render() { ... }}# Head.jsimport React, { Component } from 'react';import PropTypes from 'prop-types';export default class Head extends Component { static contextTypes = { store: PropTypes. } 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.jsimport PropTypes from 'prop-types';...export default class Body extends Component { static contextTypes = { store: PropTypes. } 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.jsimport React, { Component } from 'react';import PropTypes from 'prop-types';export default class Button extends Component { static contextTypes = { store: PropTypes. } 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 已经被各个后代组件访问到:
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. , 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. , 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. , 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" ={() => this.changeContext('HEAD')}>{this.state.headBtn}</div> <div className="btn" ={() => 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.jsimport 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.jsimport 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.jsimport React, { Component } from 'react';import PropTypes from 'prop-types';export const connect = (Comp) => { class Connect extends Component { static contextTypes = { store: PropTypes. , 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.jsimport 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" ={() => this.changeContext('HEAD', '我是改变的数据1')}>{this.props.headBtn}</div> <div className="btn" ={() => 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. , dispatch: PropTypes.func, subscribe: PropTypes.func, getStore: PropTypes.func, ...propsType } ... } return Connect;}# Head.js...const propsType = { store: PropTypes. ,}export default connect(Head, propsType);复制代码以上,我们重新改造了 connect ,让他接收两个参数,把一些固定要传递的属性,我们可以写死,然后再添加进我们在每个组件内部单独定义的 propsType。
2、Provider
我们看到,在所有的后代组件中,已经分离出了有关 context 的操作,但是,在 App.js 中,依旧还有和 context 相关的内容。其实,在 App 中用到 context 只是为了把 store 存放进去,好让后代组件可以从中获取数据。那么,我们完全可以通过容器组件来进行状态提升,把这部分脏活从 App 组件中分离出来,提升到新建的容器组件中。我们只需要给他传入需要存放进 context 的 store 就可以了。
依据之前的想法,我们在 redux 文件夹下新建一个 Provider,并把所有和业务无关的代码从 App 中取出:
# Providerimport React, { Component } from 'react';import PropTypes from 'prop-types';import { createStore, storeChange } from '../redux';export class Provider extends Component { static childContextTypes = { store: PropTypes. , 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日
原文作者:吴永辉
本文来源掘金如需转载请紧急联系作者
继续阅读与本文标签相同的文章
JS高级之面试必须知道的几个点
-
基于 HTML5 的 3D 工业互联网展示方案
2026-06-02栏目: 教程
-
用Node.js开发一个Command Line Interface (CLI)
2026-06-02栏目: 教程
-
从promise、process.nextTick、setTimeout出发,谈谈Event Loop中的Job queue
2026-06-02栏目: 教程
-
BAT前端经典面试问题:史上最最最详细的手写Promise教程
2026-06-02栏目: 教程
-
React 知识梳理(三):手写一个自己的 React-redux
2026-06-02栏目: 教程
