1. 程式人生 > >react-redux高階元件connect方法使用介紹以及實現原理

react-redux高階元件connect方法使用介紹以及實現原理

redux

這裡寫圖片描述

connect之前先來回顧一下redux的基本用法, 見下面的例子:

import { createStore } from 'redux';

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state;
  }
}

// 建立 Redux store 來存放應用的狀態。
// API 是 { subscribe, dispatch, getState }。
let
store = createStore(counter); // 可以手動訂閱更新,也可以事件繫結到檢視層。 store.subscribe(() => console.log(store.getState()) ); // 改變內部 state 惟一方法是 dispatch 一個 actionstore.dispatch({ type: 'INCREMENT' }); // 1 store.dispatch({ type: 'INCREMENT' }); // 2 store.dispatch({ type: 'DECREMENT' }); // 1

以上總結起來就只有下面四個方法

let
store = createStore(reducer); // 建立store store.getState( ); // 獲取state值 store.dispatch({ type: "text" }); // 使用action更改在reducer中定義好的更改store的更改策略 store.subScribe(render); // 設定監聽函式, 在更改store之後觸發

關於redux的更多內容, 可以參閱官方文件.
好了, 進入正題

react-redux 高階元件 connect方法介紹以及實現原理

在使用介紹connect之前, 先簡單介紹一下什麼是高階元件.

高階元件

高階元件就是一個函式,傳給它一個元件,它返回一個新的元件。 實際上就是一個類工廠, 見下面的一個例子.

import React, { Component } from 'react'

export default (People) => {
  class Star extends Component {
    // 可以做很多自定義邏輯
    render () {
      return <People/>
    }
  }
  return Star
}

這就好比一個普通的人, 經過公司的包裝之後, 變成一個會很多種技能的明星一樣.

import React, { Component } from 'react'

export default (People, things) => {
  class Star extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      ajax.get('/data/' + things, (data) => {
        this.setState({ data })
      })
    }

    render () {
      return <People data={this.state.data} />
    }
  }
  return Star
}

People推上市場之前, 先告訴公司他需要什麼things, 然後公司在componentWillMount階段對他需要的things進行準備, 最後返回一個Star

React.js 的 context

還要再說一個小概念, 如果不提這個無法講述connect的實現原理, 因此如果不想看原理只想看使用方法的話, 這一部分可以跳過, 不過博主會用簡單的語言描述這個問題, 最好看一下.ヾ(◍°∇°◍)ノ゙

某個元件只要往自己的 context 裡面放了某些狀態,這個元件之下的所有子元件都直接訪問這個狀態而不需要通過中間元件的傳遞。一個元件的 context 只有它的子元件能夠訪問,它的父元件是不能訪問到的,你可以理解每個元件的 context 就是瀑布的源頭,只能往下流不能往上飛。
那麼怎麼設定context呢? 見下面的程式碼

class Parent extends Component { // 父元件
  static childContextTypes = {
    tags: PropTypes.string
  }

  constructor () {
    super()
    this.state = { tags: 'hello' }
  }

  getChildContext () {
    return { index: this.state.tags }
  }

  render () {
    return (
      <div>
          <Title />
      </div>
    )
  }
}

class Title extends Component {
  static contextTypes = {
    tags: PropTypes.string
  }

  render () {
    return (
      <h1>{ this.context.tags }</h1>
    )
  }
}

總的來說:
一個元件可以通過 getChildContext 方法返回一個物件, 這個物件就是子樹的 context, 提供 context 的元件必須提供 childContextTypes 作為 context 的宣告和驗證.

如果一個元件設定了 context, 那麼它的子元件都可以直接訪問到裡面的內容, 它就像這個元件為根的子樹的全域性變數。任意深度的子元件都可以通過 contextTypes 來宣告你想要的 context 裡面的哪些狀態, 然後可以通過 this.context 訪問到那些狀態.

connect

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

connect有四個引數, 但是後兩個引數用到的很少, 所以本篇部落格之探討前兩個引數.

connect 的第一個引數是 mapStateToProps

這個函式允許我們將 store 中的資料作為 props 繫結到元件上

const mapStateToProps = (state) => { // 正常我們在react-redux中會這樣書寫
  return {
    themeColor: state.themeColor
  }
}
People = connect(mapStateToProps)(People) // connect返回來的是一個函式, 因此還要再一次呼叫傳入元件

實現主要原理, 就是將需要繫結的props作為一個函式傳過來, 在connect中傳給mapStateToProps一個真實的store的資料

const connect = (mapStateToProps) => (People) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = { allProps: {} }
    }

    componentWillMount () {
      const { store } = this.context
      this.setProps()
    }

    setProps () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState(), this.props) // 額外傳入 props
      this.setState({
        allProps: { // 整合普通的 props 和從 state 生成的 props
          ...stateProps,
          ...this.props
        }
      })
    }

    render () {
      return <People {...this.state.allProps} />
    }
  }
  return Connect
}

connect 的第二個引數是 mapDispatchToProps

由於更改資料必須要觸發action, 因此在這裡的主要功能是將 action 作為props 繫結到 元件上

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    increase: (...args) => dispatch(actions.increase(...args)),
    decrease: (...args) => dispatch(actions.decrease(...args))
  }
}

class People extends Component {
  render(){
    const {count, increase, decrease} = this.props;
    return (<div>
      <div>計數:{this.props.count}次</div>
      <button onClick={increase}>增加</button>
      <button onClick={decrease}>減少</button>
    </div>)
  }
}

const NiuPeople = connect(mapStateToProps, mapDispatchToProps)(People);

這裡的實現原理和上面的相差不多, 主要是將actionprops一起傳到元件裡.

const connect = (mapStateToProps, mapDispatchToProps) => (People) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount () {
      const { store } = this.context
      this.setProps()
      store.subscribe(() => this.setProps())
    }

    setProps () { // 做了一下完整性的判斷
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {} // 防止 mapStateToProps 沒有傳入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {} // 防止 mapDispatchToProps 沒有傳入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render () {
      return <People {...this.state.allProps} />
    }
  }
  return Connect
}

Provider

這是最後一個要說的問題, 講到這裡可能有一個疑問, 就是context是什麼時候設定的呢. 下面要說的就是這個問題.

Provider就是react-redux中的一個元件, Provider 做的事情也簡單, 它就是一個容器元件, 會把巢狀的內容原封不動作為自己的子元件渲染出來. 它還會把外界傳給它的 props.store 放到 context, 這樣子元件 connect 的時候都可以獲取到. 見下面程式碼.

class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return {
      store: this.props.store
    }
  }

  render () {
    return (
      <div>{this.props.children}</div>
    )
  }
}