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 的代碼隔離到一小塊地方並避免直接使用 context 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 官方現在已經廢棄了更新 context 的 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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。