React我的入門總結《五》

簡介

這一次總結的是 React-redux 的實現,能夠參考一下 大佬的文章html

首先要知道 redux 的基本使用:react

  1. 建立一個 Storegit

    <!-- store -->
    function createStore (reducer) {
      let state = null
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)
      const getState = () => state
      const dispatch = (action) => {
        state = reducer(state, action)
        listeners.forEach((listener) => listener())
      }
      dispatch({}) // 初始化 state
      return { getState, dispatch, subscribe }
    }
    
    <!-- reducer -->
    const themeReducer = (state = {}, action) => {
      switch (action.type) {
        case 'CHANGE_COLOR':
          return { ...state, themeColor: action.themeColor }
        default:
          return state
      }
    }
    
    <!-- 建立 store -->
    const store = createStore(themeReducer)
    複製代碼

    Store 是保存數據的地方,整個應用只有一個,調用 CreateStore 函數而且傳入一個 Reducer 來建立一個 Store,而且會返回新的 Store 對象。github

  2. 獲取當前的 Stateredux

    <!-- 調用 store.getState 獲取當前的狀態 -->
    const state = store.getState()
    複製代碼

    StateStore 裏面包含的數據對象,能夠經過 Store.getState() 獲取數組

  3. 經過 Dispatch 發送 Action 改變 Statebash

    <!-- 調用 dispatch 發送 action -->
    store.dispatch({
        type: 'CHANGE_COLOR',
        themeColor: 'blue'
    })
    複製代碼

    Action 就是 View 發出的通知,表示 View 要變化,其中 Type 是必須的,其他能夠 自定義app

    若是要寫多個 Action 以爲麻煩,可使用 Action Creator 函數來生產 Actiondom

    function updateThemeColor (action) {
        type: action.type,
        themeColor: action.themeColor
    }
    
    store.dispatch( updateThemeColor({ type: 'CHANGE_COLOR', themeColor: 'blue' }) )
    複製代碼
  4. ReducerStore 收到 Action 以後用來計算 State 而且返回新的 State,也就是說必需要有 Return異步

    <!-- reducer -->
    
    <!-- 初始 state 是必須的,redux 規定不能爲 undefined 和 null -->
    const themeReducer = (state = {}, action) => {
      switch (action.type) {
        case 'CHANGE_COLOR':
          return { ...state, themeColor: action.themeColor }
        default:
          return state
      }
    }
    複製代碼

    Reducer 能夠根據不一樣的 Type 來進行不一樣的邏輯處理,而且每次都會返回新的 state 來覆蓋原來的 state

    Reducer 是一個純函數,一樣的輸入就會獲得一樣的輸出。

    Reducer 必需要返回一個新的狀態,而不是改變原有的狀態,請參考下面寫法:

    // State 是一個對象
    function reducer(state, action) {
      return Object.assign({}, state, { thingToChange });
      // 或者
      return { ...state, ...newState };
    }
    
    // State 是一個數組
    function reducer(state, action) {
      return [...state, newItem];
    }
    複製代碼
  5. 調用 subscribe 傳入一個函數,狀態改變時會調用此函數。

    store.subscribe(()=> {
        ReactDOM.render()
    })
    複製代碼

    Store.subscribe 方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。

    通常傳入 renderthis.setState() 來監聽頁面從新渲染。

    調用此方法會返回一個函數,調用函數以後能夠解除監聽。


React-redux

首先以前向組件傳遞參數時,第一次使用的是 狀態提高,即經過父級傳入一個函數而後拿到組件裏面的東西,再傳入另外一個組件裏面。

當嵌套的太多層時,使用 狀態提高 會很是麻煩,而後第二次就開始使用了 Context ,因爲 Context 能隨意被改變,這時咱們能夠把 ContextStore 結合使用,這樣就不能隨意的改變 Context ,而且狀態還能共享。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
// 引入組件
import { Header } from './component/Header';
import { Content } from './component/Content';
import * as serviceWorker from './serviceWorker';
<!-- store -->
function createStore(reducer) {
  let state = null;
  const listeners = [];
  const subscribe = (listener) => listeners.push(listener);
  const getState = () => state;
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  }
  dispatch({});
  return { getState, dispatch, subscribe };
}
<!-- reducer -->
const themeReducer = (state, action) => {
  if (!state) return {
    themeColor: 'red'
  }
  switch (action.type) {
    case 'CHANGE_COLOR':
      return { ...state, themeColor: action.themeColor }
    default:
      return state
  }
}
<!-- 建立 store -->
const store = createStore(themeReducer);


class Index extends Component {
  <!-- 設置子組件的 contextType -->
  static childContextTypes = {
    store: PropTypes.object
  }
  <!-- 設置 context -->
  getChildContext() {
    return { store }
  }

  render() {
    return (
      <div className="index">
        <Header />
        <Content />
      </div>
    )
  }
}

ReactDOM.render(<Index />, document.getElementById('root'));
複製代碼

建立 store 而後把它放到 context 裏面,這樣全部子組件均可以拿到了。

<!-- Header -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Header extends Component {
  static contextTypes = {
   store: PropTypes.object 
  }

  constructor(props) {
    super(props);
    this.state = {
      themeColor: ''
    }
  }

  componentWillMount() {
    let { store } = this.context;
    this._updateThemeColor();
    store.subscribe(() => this._updateThemeColor())
  }

  _updateThemeColor() {
    let state = this.context.store.getState();
    this.setState({
      themeColor: state.themeColor
    })
  }

  render() {
    return (
      <div className="header">
        <h1 style={{color: this.state.themeColor}}>is header</h1>
      </div>
    )
  }
} 
export { Header };

<!-- Content-->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ThemeSwitch } from './ThemeSwitch';

class Content extends Component {
  static contextTypes = {
    store: PropTypes.object
  }

  componentWillMount() {
    let { store } = this.context;
    this._updateThemeColor();
    store.subscribe(() => this._updateThemeColor())
  }

  _updateThemeColor() {
    let state = this.context.store.getState();
    this.setState({
      themeColor: state.themeColor
    })
  }

  render() {
    return (
      <div className="header">
        <h2 style={{ color: this.state.themeColor }}>is Content</h2>
        <ThemeSwitch />
      </div>
    )
  }
}
export { Content };

<!-- ThemeSwitch -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class ThemeSwitch extends Component {
  static contextTypes = {
    store: PropTypes.object
  }

  componentWillMount() {
    let { store } = this.context;
    this._updateThemeColor();
    store.subscribe(() => this._updateThemeColor())
  }
  
  _updateThemeColor() {
    let state = this.context.store.getState();
    this.setState({
      themeColor: state.themeColor
    })
  }
  
  render() {
    return (
      <div className="header">
        <button style={{ color: this.state.themeColor }}>red</button>
        <button style={{ color: this.state.themeColor }}>blue</button>
      </div>
    )
  }
}

export { ThemeSwitch };
複製代碼

上面三個子組件使用 store.getState() 獲取到 reducer 設置的默認狀態,這樣的話就能夠實現共享狀態了。

接下來咱們實現點擊按鈕改變顏色:

<!-- ThemeSwitch -->
updateThemeColor(color) {
    let { store } = this.context;
    store.dispatch({
      type: 'CHANGE_COLOR',
      themeColor: color
    })
}

render() {
    return (
      <div className="header">
        <button style={{ color: this.state.themeColor }} onClick={this.updateThemeColor.bind(this, 'red')}>red</button>
        <button style={{ color: this.state.themeColor }} onClick={this.updateThemeColor.bind(this, 'blue')}>blue</button>
      </div>
    )
}
複製代碼

調用 dispatch 而後傳入 action ,而後會調用 reducer 函數,而後根據傳入的 action.type 改變狀態,以後再返回一個新的狀態。

返回新狀態時要想監聽頁面的更新,能夠在 subscribe 傳入要監聽的函數,這樣就能夠在調用 dispatch 同時會調用你傳入的函數,而後再一次調用 this.setState 觸發頁面從新渲染。

_updateThemeColor() {
    <!-- 從新獲取一次狀態 -->
    let state = this.context.store.getState();
    <!-- 從新設置,而且觸發從新渲染 -->
    this.setState({
      themeColor: state.themeColor
    })
  }
  
  componentWillMount() {
    let { store } = this.context;
    <!-- 首次渲染 -->
    this._updateThemeColor();
    store.subscribe(() => this._updateThemeColor())
  }
複製代碼

connect

上面的組件有着重複的邏輯,首先取出 storestate 而後設置成本身的狀態,還有一個就是對 context 依賴過強,這時咱們能夠利用 高階組件 來和 context 打交道,這時就不用每一個組件都獲取一遍 store 了。

import React, { Component } from 'react';
import PropTypes from 'prop-types';
<!-- 接受一個組件 -->
const connect = (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }
    
    render() {
      const { store } = this.context;
      return <WrappedComponent />
    }
  }
  return Connect;
}
export { connect };
複製代碼

connect 是用於從 UI 組件生成 容器組件 ,也就是說咱們傳入的組件只是負責呈現和展現,而 容器組件 負責業務邏輯和帶有內部狀態,connect 負責的是將二者合併起來,生成並返回新的組件。

因爲每一個傳進去的組件須要的 store 裏面的數據都不同,因此咱們還要傳入一個函數來告訴 高階組件 正確獲取數據。

  • mapStateToProps
const mapStateToProps = (state) {
    return {
        themeColor: state.themeColor
    }
}
複製代碼

mapStateToProps 是一個獲取 store 保存的狀態,而後將這個狀態轉化爲 UI組件 的數據的函數,它必需要返回一個對象,而這個對象用來進行狀態轉化的。

import React, { Component } from 'react';
import PropTypes from 'prop-types';

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

    render () {
      const { store } = this.context;
      <!-- 從store獲取state而且轉化以後所有傳入 props -->
      let stateProps = mapStateToProps(store.getState());
      return <WrappedComponent {...stateProps} />
    }
  }
  return Connect;
}

export { connect };
複製代碼

上面最關鍵的一步就是調用 mapStateToProps 時,從 store 獲取到 state 以後而後傳入到 mapStateToProps 函數中,而後這函數會返回一個轉化後的 state ,而後把這些轉化的狀態所有傳入 props 裏面。

能夠看出 connectDumb組件(純組件)context 連起來了,下面只須要調用 connect 而後傳入一個 mapStateToPropsUI組件 就可使用了。

<!-- Header -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';

class Header extends Component {
  static propTypes = {
    themeColor: PropTypes.string
  }

  render() {
    return (
      <div className="header">
        <h1 style={{color: this.props.themeColor}}>is header</h1>
      </div>
    )
  }
} 

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}

Header = connect(mapStateToProps)(Header)
export { Header };

<!-- Content -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';
import { ThemeSwitch } from './ThemeSwitch';

class Content extends Component {
  
  static propTypes = {
    themeColor: PropTypes.string
  }

  render() {
    return (
      <div className="header">
        <h2 style={{ color: this.props.themeColor }}>is Content</h2>
        <ThemeSwitch />
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}

Content = connect(mapStateToProps)(Content)
export { Content };
複製代碼

因爲 mapStateToProps 返回的對象通過 connect 傳入組件的 props 中,咱們直接能夠用 this.props 直接獲取到。

接着把 connect 的代碼複製到一個叫 React-redux 的文件,而後能夠刪掉以前那些引入 store 的代碼了。

如今點擊按鈕只有按鈕會變顏色,接下來咱們修改一下 connect

import React, { Component } from 'react';
import PropTypes from 'prop-types';

const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {

    static contextTypes = {
      store: PropTypes.object
    }

    constructor (props) {
      super(props);
      this.state = { 
        allProps: {}
      }
    }
    
    componentWillMount () {
      const { store } = this.context;
      this._updateProps();
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      <!-- 如今 mapStateToProps 能夠接受兩個參數 -->
      let stateProps = mapStateToProps(store.getState(), this.props)
      <!-- 整合普通的 props 和從 state 生成的 props -->
      this.setState({
        allProps: {
          ...stateProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent { ...this.state.allProps } />
    }
  }
  
  return Connect;
}
export { connect };
複製代碼

每次點擊按鈕調用 dispatch 都會把心的 state 設置到本身的 state 以後,而後返回給組件,這樣組件以前的 props 也會保留,同時 mapStateToProps 能夠接受第二個參數,這個參數爲當前 UI組件props

<!-- 第一個爲 store 獲取到的 state , 第二個爲當前 ui 組件的 props (不是最新的) -->
const mapStateToProps = (state, props) => {
  console.log(state, props)
  return {
    themeColor: state.themeColor
  }
}
複製代碼

使用 props 做爲參數後,若是容器組件的參數發生變化,也會引起 UI組件 從新渲染,connect 方法能夠省略 mapStateToProps 參數,這樣 store 的更新不會引發組件的更新。

  • mapDispatchToProps
const mapDispatchToProps = (dispatch, props) => {
    return {
        updateThemeColor: () => {
            dispatch({
                type: 'CHANGE_COLOR',
                payload: ''
            })
        }
    }
}
複製代碼

mapDispatchToPropsconnect 的第二個參數,用來創建 UI組件 的參數到 store.dispatch 方法的映射,它做爲函數時能夠接受兩個參數,一個是 dispatch ,一個則是 UI組件props

mapDispatchToProps 能夠定義 action 而後傳給 store

import React, { Component } from 'react';
import PropTypes from 'prop-types';

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

    constructor (props) {
      super(props);
      this.state = { 
        allProps: {}
      }
    }
    
    componentWillMount () {
      const { store } = this.context;
      this._updateProps();
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {}
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {}
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }
    render () {
      return <WrappedComponent { ...this.state.allProps } />
    }
  }
  return Connect;
}
export { connect };
複製代碼

接受 mapDispatchToProps 做第二個參數,調用時把 dispatchprops 傳進去,返回 onClickUpdate 而後直接傳入 props 中返回給 UI組件 ,接着咱們能夠直接調用 this.props.onClickUpdate 而後調用 dispatch 來更新狀態。

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';

class ThemeSwitch extends Component {
  static contextTypes = {
    onClickUpdate: PropTypes.func
  }
  <!-- 點擊調用 onClickUpdate -->
  updateThemeColor(color) {
    if(this.props.onClickUpdate) {
      this.props.onClickUpdate(color)
    }
  }

  render() {
    return (
      <div className="header">      
        <button style={{ color: this.props.themeColor }} onClick={this.updateThemeColor.bind(this, 'red')}>red</button>
        <button style={{ color: this.props.themeColor }} onClick={this.updateThemeColor.bind(this, 'blue')}>blue</button>
      </div>
    )
  }
}
<!-- 在真正的 react-redux 不必定是函數 -->
const mapStateToProps = (state, props) => {
  return {
    themeColor: state.themeColor
  }
}
<!-- 在真正的 react-redux 能夠是一個對象 -->
const mapDispatchToProps = (dispatch, props) => {
  return {
    onClickUpdate: (color) => {
      dispatch({
        type: 'CHANGE_COLOR',
        themeColor: color
      })
    }
  }
}

ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch);
export { ThemeSwitch };
複製代碼

這樣點擊按鈕以後又能夠改變顏色了。

Provider

connect 方法生成容器後須要拿到 state 對象,目前我們能拿到 store 是由於在 index.js 中設置了 context ,這樣會直接污染 index.jsReact-redux 提供了 Provider 來充當最外層容器,這樣就不須要在 index 設置 context 了。

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>
    )
  }
}

export { Provider };
複製代碼

React-redux 的文件增長上面的代碼,其實也就是另外設置一個容器來替代以前 index.js 乾的活,這裏返回了 this.props.children ,也說明要用這個組件把其餘的組件包起來。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { Provider } from './component/React-redux';
// 引入組件
import { Header } from './component/Header';
import { Content } from './component/Content';
import * as serviceWorker from './serviceWorker';

function createStore(reducer) {
  let state = null;
  const listeners = [];
  const subscribe = (listener) => listeners.push(listener);
  const getState = () => state;
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  }
  dispatch({});
  return { getState, dispatch, subscribe };
}

const themeReducer = (state, action) => {
  if (!state) return {
    themeColor: 'red'
  }
  switch (action.type) {
    case 'CHANGE_COLOR':
      return { ...state, themeColor: action.themeColor }
    default:
      return state
  }
}

const store = createStore(themeReducer);

class Index extends Component {
  render() {
    return (
      <div className="index">
        <Header />
        <Content />
      </div>
    )
  }
}


ReactDOM.render(
  <!-- 把 store 和 外層組件包起來 -->
  <Provider store= { store }>
    <Index />
  </Provider>, 
  document.getElementById('root')
);
複製代碼

Store 傳入給 Provider ,而後它把 store 設置成 context ,這樣其餘子組件都能拿到 store ,而且把最外層容器包起來,而後使用 this.props.children 所有羅列出來。

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';

class Header extends Component {
  <!-- 別忘了聲明這玩意,否則拿不到 -->
  static contextTypes = {
    store: PropTypes.object
  }

  static propTypes = {
    themeColor: PropTypes.string
  }

  componentWillMount() {
    console.log(this.context)
  }

  render() {
    return (
      <div className="header">
        <h1 style={{color: this.props.themeColor}}>is header</h1>
      </div>
    )
  }
} 

const mapStateToProps = (state, props) => {
  return {
    themeColor: state.themeColor
  }
}

Header = connect(mapStateToProps)(Header)
export { Header };
複製代碼

拿到以後接下來就能夠浪了,能夠在當前組件調用裏面的方法,很是靈活。

總結

  1. 首先在 index.js 引入建立好的 store ,而後引入 Providerindex 包起來,而且給它傳遞 store
  2. 若是頁面須要拿到狀態直接調用 store.getState ,若是想監聽函數調用 store.subscribe 傳入函數。
  3. 若是想訂閱 store 或者修改 state ,在當前組件引入 connect 接着傳入 mapStateToPropsmapDispatchToProps 來呈現新的 UI組件
  4. dispatch 還能夠拓展,真正的 react-redux 還可使用中間件實現異步 action ,如須要從後臺返回的狀態來改變當前的 state 相似這種操做。
  5. 可使用 combineReducers 管理多個 reducer,一個 store 管理N種狀態。

上一篇 --- React我的入門總結《四》

下一篇 --- React我的入門總結《六》

相關文章
相關標籤/搜索