從零開始寫一個 redux(第二講)

相關倉庫: github.com/mcuking/min…javascript

本講主要解決如何在 react 中更優雅的使用 redux,即實現 react-reduxvue

Provider

在實現 react-redux 以前,咱們首先須要瞭解 react 的 context 機制。當須要將某個數據設置爲全局,便可使用 context 在父組件聲明,這樣其下面的全部子組件均可以獲取到這個數據。java

(注意,在最新的 react 16.3(.0-alpha)中,context 機制已經更新,功能更增強大,詳情請參考個人譯文 React 16.3(.0-alpha)新特性-譯)react

基於 context 機制,咱們定義一個 Provider,做爲應用的一級組件,專門負責將傳入的 store 放到 context 裏,全部子組件都可以直接獲取 store,並不渲染任何東西。git

// Provider 負責將store放到context裏,全部子組件都可以直接獲取store
export class Provider extends Component {
  // 使用context須要使用propType進行校驗
  static childContextTypes = {
    store: PropTypes.object
  };

  constructor(props, context) {
    super(props, context);
    this.store = props.store; // 將傳進來的store做爲自己的store進行管理
  }

  // 將傳進來的store放入全局context,使得下面的全部子組件都可得到store
  getChildContext() {
    return { store: this.store };
  }

  render() {
    return this.props.children;
  }
}
複製代碼

對應業務代碼以下:github

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

const store = createStore(counter);
ReactDOM.render(
  <Provider store={store}> <App /> </Provider>,
  document.getElementById('root')
);
複製代碼

connect

connect 負責鏈接組件,將 redux 中的數據傳入組件的屬性裏,所以須要完成下面兩件事:redux

  1. 負責接收一個組件,並將組件對應全局 state 裏的一些數據放進去,返回一個新組件
  2. 數據變化時,可以通知組件

所以 connect 自己是一個高階組件,首先接收下面兩個參數,而後再接收一個組件:bash

  • mapStateToProps,是一個函數,入參爲全局 state,並返回全局 state 中組件須要的的數據,代碼以下:
const mapStateToProps = state => {
  return {
    num: state
  };
};
複製代碼
  • mapDispatchToProps,是一個對象,對象裏面爲 action(用來改變全局狀態的對象)的生成函數,代碼以下:
const mapDispatchToProps = {
  buyHouse,
  sellHouse
};

// action creator
export function buyHouse() {
  return { type: BUY_HOUSE };
}

export function sellHouse() {
  return { type: SELL_HOUSE };
}
複製代碼

第一步,咱們將 mapStateToProps 的返回值,即組件須要的全局狀態 state 中的某個狀態,以參數的形式傳給新構建的組件,代碼以下:dom

export const connect = (
  mapStateToProps = state => state,
  mapDispatchToProps = {}
) => WrapComponent => {
  // 高階函數,連續兩個箭頭函數意味着嵌套兩層函數
  return class ConnectComponent extends Component {
    // 使用context須要使用propType進行校驗
    static contextTypes = {
      store: PropTypes.object
    };

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

    componentDidMount() {
      this.update();
    }

    // 獲取mapStateToProps返回值,放入到this.state.props
    update() {
      const { store } = this.context; // 獲取放在全局context的store

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

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

    render() {
      return <WrapComponent {...this.state.props} />; } }; }; 複製代碼

第二步,咱們須要將 mapDispatchToProps 這個對象中修改全局狀態的方法傳入給組件,可是直接將相似 buyHouse 方法傳給組件,並在組件中執行 buyHouse()方法並不能改變全局狀態。ide

聯想到上一講 redux 中,修改全局狀態,須要使用 store 的 dispatch 方法,dispatch 對應代碼以下:

function dispatch(action) {
  // reducer根據老的state和action計算新的state
  currentState = reducer(currentState, action);

  // 當全局狀態變化時,執行傳入的監聽函數
  currentListeners.forEach(v => v());
  return action;
}
複製代碼

其中須要外部傳入 action,即一個對象,例如{type: BUY_HOUSE}。所以咱們須要將 buyHouse 方法的返回值 action 對象,傳給 store.dispatch 方法,執行後才能改變全局狀態。對應代碼以下:

buyHouse = () => store.dispatch(buyHouse());
複製代碼

對此,咱們封裝一個方法 bindActionCreators,入參爲 mapDispatchToProps 和 store.dispatch,返回相似 buyHouse = () => store.dispatch(buyHouse())的方法的集合,即便用 dispatch 將 actionCreator 的返回值包一層,代碼以下:

// 將 buyHouse(...arg) 轉換爲 (...arg) => store.dispatch(buyHouse(...arg))
function bindActionCreator(creator, dispatch) {
  return (...arg) => dispatch(creator(...arg)); // 參數arg透傳
}

// creators 示例 {buyHouse, sellHouse, buyHouseAsync}
export function bindActionCreators(creators, dispatch) {
  let bound = {};
  for (let v in creators) {
    bound[v] = bindActionCreator(creators[v], dispatch);
  }
  return bound;
}
複製代碼

所以,咱們就能夠第一步的基礎上,將 store.dispatch 包裝後的 actionCreator 集合對象,傳給組件,代碼以下:

export const connect = (
  mapStateToProps = state => state,
  mapDispatchToProps = {}
) => WrapComponent => {
  // 高階函數,連續兩個箭頭函數意味着嵌套兩層函數
  return class ConnectComponent extends Component {
    // 使用context須要使用propType進行校驗
    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();
    }

    // 獲取mapStateToProps的返回值 和 mapDispatchToProps,放入到this.state.props
    update() {
      const { store } = this.context; // 獲取放在全局context的store

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

      // 方法不能直接傳,由於須要dispatch
      // 例如直接執行addHouse()毫無心義,須要buyHouse = () => store.dispatch(addHouse())纔有意義
      // 其實就使用diapatch將actionCreator包了一層
      const dispatchProps = bindActionCreators(
        mapDispatchToProps,
        store.dispatch
      );

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

    render() {
      return <WrapComponent {...this.state.props} />; } }; }; 複製代碼

注意,除了將 dispatchProps 傳給組件以外,上面代碼還在組件的 componentDidMount 生命週期中,將 update 函數設置爲監聽函數,即

store.subscribe(() => this.update())
複製代碼

從而,每當全局狀態發生變化,都會從新獲取最新的傳入組件的狀態和方法,實現組件狀態與全局狀態同步的效果。

另外最近正在寫一個編譯 Vue 代碼到 React 代碼的轉換器,歡迎你們查閱。

github.com/mcuking/vue…

相關文章
相關標籤/搜索