banner
他山之石

他山之石

手摸手撸一个简单的Redux(二)

理解 Redux 的原理有助于我们更好的使用它。本文实现 react-redux 的功能。

image

在上一篇文章中,实现了一个简单的 Redux,主要是对它的 API 进行了实现。本文将会实现一个简单的 react-redux。

本文完整代码请查看 Github:https://github.com/YanYuanFE/redux-app

// clone repo
git clone https://github.com/YanYuanFE/redux-app.git


cd redux-app

// checkout branch
git checkout part-4

// install
npm install

// start
npm start

使用 react 开发应用时,通常使用 props 来进行组件之间的数据传递,但是,当你的应用组件层级嵌套很深时,如果需要从根组件传递数据到最里层的组件,你可能需要向下每层都手动地传递你需要的 props,这时,你需要 react 提供的 context API。

react 官方并不建议使用 context API,因为 context 是一个实验性的 API,在未来的 react 版本中可能会被更改。到目前为止,react 16 的最新版本已经更改了 context API。

尽管有官方的警告,但是仍然有需要使用到 context 的场景。一个比较好的做法是将 context 的代码隔离到一小块地方并避免直接使用 cntext API,这样以后 API 变更的时候更容易升级。这也是 react-redux 的做法。

Context 的用法#

考虑如下代码:

const PropTypes = require('prop-types');

class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.context.color}}>
        {this.props.children}
      </button>
    );
  }
}

Button.contextTypes = {
  color: PropTypes.string
};

class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text} <Button>Delete</Button>
      </div>
    );
  }
}

class MessageList extends React.Component {
  getChildContext() {
    return {color: "purple"};
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

MessageList.childContextTypes = {
  color: PropTypes.string
};

上述代码包含三个组件,顶层组件 MessageList 包含多个 Message 组件,每个 Message 组件中包含了 Button 组件。如果需要从顶层的 MessageList 组件中传递 color 属性到 Button 组件,需要手动将 color 属性通过 props 传递到 Message,然后再从 Message 传递到 Button 组件中。上述代码使用了 Context API 来实现。首先需要一个 context 提供者,在这里是 MessageList,MessageList 组件需要添加 getChildContext 方法和 childContextTypes 等官方 API。getChildContext 方法用于返回全局的 context 对象,childContextTypes 用于定义 context 属性的类型。React 会向下自动传递 context 参数,任何组件只要在它的子组件中,就可以通过定义 ContextTypes 来获取 context 参数。

更新 Context#

react 官方现在已经废弃了更新 contetx 的 API,为了更新 context 的数据,可以使用 this.setState 来更新本地 state,当 state 或者 props 更新时,getChildContext 会自动调用。将会生成一个新的 context,所有子组件都会收到更新。
考虑如下代码:

const PropTypes = require('prop-types');

class MediaQuery extends React.Component {
  constructor(props) {
    super(props);
    this.state = {type:'desktop'};
  }

  getChildContext() {
    return {type: this.state.type};
  }

  componentDidMount() {
    const checkMediaQuery = () => {
      const type = window.matchMedia("(min-width: 1025px)").matches ? 'desktop' : 'mobile';
      if (type !== this.state.type) {
        this.setState({type});
      }
    };

    window.addEventListener('resize', checkMediaQuery);
    checkMediaQuery();
  }

  render() {
    return this.props.children;
  }
}

MediaQuery.childContextTypes = {
  type: PropTypes.string
};

在 getChildContext 中,返回一个 context 对象,其值为 this.state.type,当你需要更新 context 时,调用 this.setState 更新 state,state 更新后,会自动执行 getChildContext 返回新的 context。

react-redux 实现#

Provider 实现#

在前面的文章已经介绍了 react-redux 的使用,react-redux 的 API 主要包括 connect 和 Provider。首先来看一下 Provider 的实现。

回顾 Provider 的用法。

import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById('root')
);

从上述代码可以看到,Provider 是一个组件,包裹在应用的根组件,接收一个 store 的 props,在 react-redux 中,Provider 组件提供 context。

接上一篇文章的项目,在 src 目录下新建 react-redux.js,首先声明 Provider 组件。

import React from 'react';
import PropTypes from 'prop-types';

export class Provider extends React.Component {

}

Provider 组件没有自己的 UI 渲染逻辑,只负责处理 context 部分逻辑。

export class Provider extends React.Component {
  static childContextType = {
    store: PropTypes.object
  }
  constructor(props, context) {
    super(props, context)
    
    this.store = props.store

  }
  render() {

    return this.props.children

  }
}

这一步,在静态方法 childContextTypes 中定义 context 属性 store 的类型为 object,在 constructor 构造函数中,传入 props 和 context,定义 this.store 并赋值为 props.store。这样,在 Provider 中任何地方都可以使用 this.store 获取到 props 中的 store 属性。
由于 Provider 不负责 UI 渲染,在 render 方法中,直接返回 this.props.children 即可,即返回子组件。

最后在 Provider 中,还需要添加 getChildContext 方法,用于提供 context。

export class Provider extends React.Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext() {
    return {store: this.store}
  }
  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }
  render() {
    return this.props.children
  }
}

在 getChildContext 中,生成 context 对象,此处的 context 就是 this.store,让子组件能够获取到 context。

connect 实现#

在 react-redux 中,connect 负责连接组件,接受一个组件作为参数,将 store 中的属性传入到组件的 props 中,并且返回一个新的组件,这种组件设计模式称为高阶组件。当数据变化时,connect 将会通知组件更新。

回顾 connect 的使用。

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => {
    dispatch(setVisibilityFilter(ownProps.filter))
  }
})

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

connect 首先定义为一个高阶函数,
在 react-redux.js 中,首先定义 connect 方法:

export const connect = (mapStateToProps = state => state, mapDispatchToProps={}) => (WrapComponent) => {
    return class ConectComponent extends React.Component {
    }
}

connect 是一个两层的箭头函数,第一层,传入 mapStateToProps 和 mapDispatchToProps 参数,这两个参数是可选参数,需要定义初始值,mapStateToProps 定义为函数,mapDispatchToProps 有多种参数形式,可以是函数或者对象,这里默认设为空对象。connect 方法最终应该返回一个组件,在第二层函数中,传入一个组件作为参数,并返回一个新的组件。上述代码可以改写为如下:

export function connect(mapStateToProps, mapDispatchToProps) {
  return function (wrapComponent) {
    return class ConnectComponent extends React.Component {

    }
  }
}

这样看起来就很清晰了,connect 首先执行最外层返回一个函数,然后传入一个组件,执行最里层,返回一个组件。
在返回的组件内部,需要获取 context,代码如下:

static contextTypes = {
  store: PropTypes.object
}

然后是 constructor 的实现:

constructor(props, context) {
  super(props, context);
  this.state = {
    props: {}
  }
}

在 constructor 中,定义了一个 props 属性作为 state,初始化为空对象。props 将传递到 wrapComponent 上。

在 render 函数中:

render() {
  return <WrapComponent {...this.state.props}/>
}

在 render 函数中,将 state.props 解构传递到 WrapComponent 的 props 属性中。当然,state.props 并没有如此简单,还需要将 mapStateToProps 和 mapDiapatchToProps 的数据注入进去。
在 componentDidMount 中,代码如下:

componentDidMount() {
  const { store } = this.context;
  store.subscribe(() => this.update());
  this.update();
}

在上述代码中,首先获取到 context 中的 store,然后调用 update 方法来更新 state。同样,在 store.subscribe 中传入 store 更新后的方法,当 store 更新后需要调用 update 方法。update 方法如下:

update() {
  const { store } = this.context;
  const stateProps = mapStateToProps(store.getState());

  this.setState({
    props: {
      ...this.state.props,
      ...stateProps,
    }
  })
}

在 update 方法中,首先从 context 中获取到 store,考虑需要 connect 的数据分为两部分,第一部分是将 state 中的数据映射到 props 中,调用 connect 的第一个参数 mapStateToProps 传入 store.getState(),即传入全局的 state。然后得到 stateProps 对象用于传入 props 中。然后通过 this.setState 来更新 state,这里通过对象延展语法来对对象进行解构合并新旧 state。

上面只实现了 state 数据的映射,还需要方法的映射,数据的映射较为简单,而方法不能直接使用,因为需要对方法调用 store.dispatch。这里需要在 redux 中实现一个 bindActionCreators 方法,按如下方式调用:

import { bindActionCreators } from './redux';



const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);

bindActionCreators 用于将 store.dispatch 传递到函数内部,调用函数时能够在内部 dispatch 该函数。

在 src 目录下的 redux.js 下,实现 bindActionCreators 方法:

export function bindActionCreators(creators, dispatch) {
  let bound = {};
  Object.keys(creators).forEach(v => {
    let creator = creators[v];
    bound[v] = bindActionCreator(creator, dispatch);
  })
  return bound;
}

在 bindActionCreators 方法中,传入 connect 的第二个参数 mapDispatchToProps 定义为 creators,creators 是一个对象,在这里需要对 creators 中的每一个方法使用 dispatch 进行一次包装,使用 Object.keys 返回对象可枚举属性组成的数组,然后循环数组,根据数组索引,依次取数组索引对应的方法,然后调用 bindActionCreator 返回一个由 dispatch 包装的新的方法,并且组装为一个新的对象 bound 返回,key 保持不变。
下面实现 bindActionCreator:

function bindActionCreator(creator, dispatch) {
  return (...args) => dispatch(creator(...args))
}

在 bindActionCreator 方法中,使用高阶函数返回了一个新的函数,原函数 creator 经 dispatch 包装后,使用剩余参数...args 透传到被包装函数内。这样是为了保证参数能够传递到最内层。
到这里,已经实现了 connect 的两个部分的数据,下面是完整的 update 方法:

update() {
  const { store } = this.context;
  const stateProps = mapStateToProps(store.getState());

  const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);

  this.setState({
    props: {
      ...this.state.props,
      ...stateProps,
      ...dispatchProps,
    }
  })
}

最终的 update 方法中,将 stateProps 和 dispatchProps 都更新到 state.props 中,这样,每次数据更新都能通知到子组件进行同步更新。
现在,react-redux 的基本功能已经实现了,下面是完整的 react-redux 代码:

import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from './redux';

export const connect = (mapStateToProps = state => state, mapDispatchToProps={}) => (WrapComponent) => {
  return class ConectComponent extends React.Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor(props, context) {
      super(props, context);
      this.state = {
        props: {}
      }
    }
    componentDidMount() {
      const { store } = this.context;
      store.subscribe(() => this.update());
      this.update();
    }
    update() {
      const { store } = this.context;

      const stateProps = mapStateToProps(store.getState());

      const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);

      this.setState({
        props: {
          ...this.state.props,
          ...stateProps,
          ...dispatchProps,
        }
      })
    }
    render() {
      return <WrapComponent {...this.state.props}/>
    }
  }
}

export class Provider extends React.Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext() {
    return {store: this.store}
  }
  constructor(props, context) {
    super(props, context);
    this.store = props.store;
  }
  render() {
    return this.props.children
  }
}

使用 react-redux 来改写计数器应用#

在之前的文章中,使用自己编写的 redux 实现了一个简单的计数器应用,现在将它改写为 react-redux 实现:

App.js如下:

import React, { Component } from 'react';
import { connect } from './react-redux';
import Counter from './components/Counter';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    const { onIncrement, onDecrement, counter } = this.props;

    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <Counter
          value={counter}
          onIncrement={onIncrement}
          onDecrement={onDecrement}
        />
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  counter: state
});

function onIncrement() {
  return { type: 'INCREMENT' }
}

function onDecrement() {
  return { type: 'DECREMENT' }
}

const mapDispatchToProps = {
  onIncrement,
  onDecrement
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

注意,由于在 react-redux 中 connect 中对 mapDispatchToProps 的处理仅考虑了其值为对象的情况,而实际可以支持对象或者函数作为参数,故在这里需要设置为对象的形式。

index.js 代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from './react-redux';
import './index.css';
import App from './App';
import { createStore } from './redux';
import counter from './reducers';

const store = createStore(counter);

ReactDOM.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById('root')
);

最后,npm start 运行项目,打开浏览器界面如下,对其进行操作,符合预期效果:

image

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。