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

react

上一次我们简单了解了一下 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日

原文作者:吴永辉

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

以上是 React 知识梳理(三):手写一个自己的 React-redux 的全部内容, 来源链接: utcz.com/z/382868.html

回到顶部