Redux、 React-Redux學(xué)習(xí)

前言

在學(xué)習(xí) React 過程中,都會接觸使用到 Redux, React-Redux ,不熟悉的小伙伴,可能疑惑有了 Redux,為什么還出現(xiàn)了 React-Redux,先帶大家了解 Redux 的使用,以及使用過程中有哪些吐槽的點,再看看 React-Redux 為啥出現(xiàn)

一、Redux

在開始之前,需要記住的是

Redux 是一款著名 JavaScript 狀態(tài)管理容器

也就是說,Redux 除了跟 React 配合使用,還可以配置 JS 、Vue 使用

1.1 設(shè)計思想

  • Redux 是將整個應(yīng)用狀態(tài)存儲到一個叫做 store 的地方,里面存在一個狀態(tài)樹 state tree
  • 組件可通過 store.dispatch 派發(fā)行為 actionstore, store 不會直接修改 state, 而是通過用戶編寫的 reducer 來生成新的 state ,并返回給 store
  • 其它組件通過訂閱 store 中的 state 狀態(tài)變化來刷新自己的視圖
redux-flow.png

1.2 三大原則

  • 整個應(yīng)用有且僅有一個 store, 其內(nèi)部的 state tree 存儲整個應(yīng)用的 state
  • state 是只讀的,修改 state,只能通過派發(fā) action,為了描述 action 如何改變 state 的,需要編寫 reducer 純函數(shù)
  • 單一數(shù)據(jù)源的設(shè)計讓 React 組件之間通信更加方便,也有利于狀態(tài)的統(tǒng)一管理

1.3 createStroe

通過實戰(zhàn)寫個 Counter 計數(shù)器來學(xué)習(xí)下 Redux 相關(guān)的 API

1.3.1 store

// ./src/store.js
import {createStroe} from 'react'

function reudcer(){}
let store = createStore(reudcer)

通過 createStore 方法可以創(chuàng)建一個 store, 需要傳遞一個參數(shù) reducer (ps: 后續(xù)介紹),而 store 是個對象,有以下方法可調(diào)用

  • store.getState(), 獲取最新的 state tree
  • store.dispatch(), 派發(fā)行為 action
  • store.subscribe(), 訂閱 store 中 state 的變化

1.3.2 reducer

reducer 必須是個純函數(shù),接收 state, action 兩個參數(shù),state 是舊的狀態(tài),不可直接修改,而是需要根據(jù) action.type 不同,來生成新的 state 并返回

// ./src/store.js
import {createStroe} from 'react'
export const ADD = 'ADD'
export const MINUS = 'MINUS'
function reducer (state = {count: 0}, action) {
  console.log('action', action); // {type: 'xxx'}  
  switch(action.type) {
    case ADD:
      return {count: state.count + 1}
    case MINUS:
      return {count: state.count - 1}
    default:
      return state
  }
}

let store = createStore(reudcer)
export default store

注意上面代碼中,給 state 設(shè)置了初始值 {count: 0}, 接下來,會在 Counter 組件中去使用這個導(dǎo)出的 store

1.3.3 getState、dispatch、subscribe

// ./src/components/Counter.jsx
import React from 'react'
import store from '../store'

class Counter extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      number: store.getState().count
    }
  }
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
      <button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>
    </div>
  }
}

export default Counter

Counter 組件中,通過 store.getState() 可獲取最新的 state, 點擊按鈕,會通過 store.dispatch 派發(fā) action 給 store (ps:請注意 action 是個對象,必須存在 type 屬性),store 內(nèi)部會將當(dāng)前 state, action 傳遞給 reducer 來生成新的 state 達到更新狀態(tài)的目的, 遺憾的是,頁面上數(shù)字并沒有發(fā)生變化

截屏2020-11-14下午1.46.43.png

可以看到,reducer 函數(shù)中已經(jīng)接受到了 action, 此時 store 中的 state 已經(jīng)發(fā)生了變化,而頁面不更新的原因在于 Counter 沒有訂閱 store 中 state 的變化,可在代碼中加入下面代碼

class Counter extends React.Component{
  componentDidMount () {
    this.unSubscribe = store.subscribe(() => {
      this.setState({
        number: store.getState().count
      })
    })
  }
  componentWillUnmount () {
    this.unSubscribe && this.unSubscribe()
  }
}

使用 store.subscribe 就可實現(xiàn)訂閱,該方法接受一函數(shù),當(dāng) storestate 中狀態(tài)發(fā)生變化,就會執(zhí)行傳入的函數(shù),同時 store.subscribe 方法返回一個函數(shù),用于取消訂閱。

至此,Counter組件已基本實現(xiàn)了??赡苡行┬』锇榘l(fā)現(xiàn)應(yīng)用首次加載后,控制臺輸出了

action {type: "@@redux/INIT1.s.m.m.c.n"}

這是 store 為了拿到 state 的初始值 {count: 0}, 會自動派發(fā)一次 action {type: "@@redux/INIT1.s.m.m.c.n"}

熟悉“發(fā)布-訂閱”模式的小伙伴可能看得出,Redux 內(nèi)部就是使用了“發(fā)布-訂閱”模式。接下來,我們嘗試實現(xiàn)個簡陋版本 Redux

1.3.4 手寫實現(xiàn) createStroe


function createStore(reducer){
    let state
  const listeners = []

  // 返回最新的 state
  function getState () {
    return state
  }

  // 派發(fā) action
  function dispatch(action){
    state = reducer(state, action)
    listeners.forEach(listener => listener())
  }

  // 訂閱,返回取消訂閱函數(shù)
  function subscribe(listener){
    listeners.push(listener)
    return function () {
        const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  // 獲取state默認(rèn)值
  dispatch({type: "@@redux/INIT1.s.m.m.c.n"})

  // 返回 store, 一個對象
  return {
    getState,
    dispatch,
    subscribe
  }
}

export default createStore

通過測試,我們簡陋版 Redux 已經(jīng)實現(xiàn)的 Counter 組件的功能

1.4 bindActionCreators

1.4.1 原理及使用

在 Counter 組件中,我們是直接使用 store.dispatch 派發(fā)action

<button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
<button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>

上面寫法的缺陷在于,多次重復(fù)寫了 store.dispatch, 并且 action.type 容易寫錯還不易發(fā)現(xiàn),此時 redux 提供了 bindActionCreators 功能,將派發(fā) action 的函數(shù)與 store.dispatch 進行綁定

// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'

// add 函數(shù)返回 action, 所以該函數(shù)可稱作 actionCreator
function add() {
  return {type: 'ADD'}
}

function minus() {
  return {type: 'MINUS'}
}

const bindAdd = bindActionCreators(add, store.dispatch)
const bindMinus = bindActionCreators(minus, store.dispatch)

class Counter extends React.Component{
    // ...
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={bindAdd}>+</button>
      <button onClick={bindMinus}>-</button>
    </div>
  }
}

export default Counter

其實,??代碼中可將 bindActionCreators 邏輯抽離到單獨文件中,可在其它組件中去使用。同時,上面代碼的缺陷在與 每個函數(shù)都需要去手動綁定,并不合理,所以,bindActionCreators 支持傳入對象,將所以的 actionCreator 函數(shù)包裝成對象

// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'

// add 函數(shù)返回 action, 所以該函數(shù)可稱作 actionCreator
function add() {
  return {type: 'ADD'}
}

function minus() {
  return {type: 'MINUS'}
}

const actions = {add, minus}

const bindActions = bindActionCreators(actions, store.dispatch)

class Counter extends React.Component{
  // ...
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={ bindActions.add }>+</button>
      <button onClick={ bindActions.minus }>-</button>
    </div>
  }
}

export default Counter

1.4.2 手寫實現(xiàn)

function bindActionCreators (actionCreater, dispatch) {
  // actionCreater 可以是函數(shù)/對象
  if (typeof actionCreater === 'function') {
    return function (...args) {
        return dispatch(actionCreater(...args))
    }
  } else {
    let bindActionCreaters = {}
    Object.keys(actionCreater).forEach(key => {
        bindActionCreaters[key] = function (...args) {
        return dispatch(actionCreater(...args))
      }
    })
    return bindActionCreaters
  }
}

export default bindActionCreaters

1.5 combineReducers

1.5.1 原理及使用

當(dāng)一個應(yīng)用包含多個模塊,將所以模塊的 state 放在并不合理,更好的做法是按照模塊進行劃分,每個模塊有各自的 reducer、action,最終通過 Redux 中的 combineReducers 合并成一個大的 reducer

// src\store\reducers\index.js
import {combineReducers} from 'redux';
import counter1 from './counterReducer1';
import counter2 from './counterReducer2';
export default combineReducers({
    x: counter1,
    y: counter2
});

// src/store/reducers/counterReducer1.js
import * as types from '../action-types';
export default function (state= {count: 0},action){
    switch(action.type){
        case types.ADD1:
            return state.count + 1;
        case types.MINUS1:
            return state.count - 1;
        default:
            return state;
    }
}

// src/store/reducers/counterReducer2.js
import * as types from '../action-types';
export default function (state= {count: 0},action){
    switch(action.type){
        case types.ADD2:
            return state.count + 1;
        case types.MINUS2:
            return state.count - 1;
        default:
            return state;
    }
}

combineReducers 方法接受一個對象,屬性key 可任意設(shè)置,屬性value對應(yīng)每個模塊的 reducer 函數(shù), 返回最終的一個合并之后的 reducer 方法。

通過 reducer 合并之后,store 中的 state tree 也會按照模塊進行劃分

store.getState() 
{
  x: {count: 0}
  y: {count: 0}
}

這樣,在組件中,使用 state 需要修改成下面這樣

import store from '../store';
export default class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
          value: store.getState().x.count
        }
    }
    //...
}

當(dāng)組件中派發(fā) action 時,action 會傳遞到 combineReducers 返回的函數(shù)中,在該函數(shù)中,會調(diào)用每個模塊各自的 reducer 生成各自新的 state, 最終將所以 state 合并之后,去更新 store 中的 state

1.5.2 手寫實現(xiàn)

function combineReducers(reducers){
  // 返回合并之后的 reducer 函數(shù)
  return function (state, action){
    const nextState = {}
    Object.keys(reducers).forEach(key => {
        nextState[key] = reducers[key](state[key], action)
    })
    return nextState
  }
}

可以看出,主要派發(fā) action,每個模塊的的 reducer 函數(shù)都會執(zhí)行的

1.6 小結(jié)

可以看出,在 React 組件中使用 store, 都需要手動去引入 store 文件, 手動訂閱 store 中狀態(tài)的變化,這是不合理的,接下來,我們看下 react-redux 是如何解決的

二、React-Redux

2.1 原理及使用

react-redux 提供一個 Provider 組件,通過 Provider 組件,可以向其子組件、孫組件傳遞 store, 而不需要每個組件都手動引入

// ./src/index.js
import { Provider } from 'react-redux'
import store from './store'

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

在后代組件 Counter1 中,可使用 react-redux 提供 connect 函數(shù),將 store 與 Counter1 組件的 props 進行關(guān)聯(lián)

import React from 'react'
import { connect } from 'react-redux'
import action from '../store/actions/Counter1'

class Counter1 extends React.Component{
  render () {
    return <div>
      <p>{ this.props.count }</p>
      <button onClick={ this.props.add }>+</button>
      <button onClick={ this.props.minus }>-</button>
    </div>
  }
}

const mapStateToProps = state => state
const mapDispatchToProps = {
  ...action
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter1)

從上面代碼中,可以看出在 Counter1 組件內(nèi)部,屬性或方法都是通過 props 訪問,我們完全可以將 Counter1 組件轉(zhuǎn)換成函數(shù)組件(無狀態(tài)組件),通過函數(shù)組件外部都是一個容器組件(有狀態(tài)組件)進行包裹,所有 connect(mapStateToProps, mapDispatchToProps)(Counter1) 最終返回的就是一個容器組件,接下來我們看下如何手寫一個 react-redux

2.2 手寫實現(xiàn)

想跨組件傳遞 store,react-redux 內(nèi)部使用了 React Context API

創(chuàng)建一個 ReactReduxContext 上下文對象

// src/react-redux/Context.js

import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext

在 Proveider 組件中,需要使用 ReactReduxContext 對象中提供的 Provider 組件

// src/react-redux/Provider.js
import React from 'react'
import {ReactReduxContext} from './Context'

class Provider extends React.Component{
  constructor(props) {
    super(props)
  }
  render () {
    return <ReactReduxContext.Provider value={{ store: this.props.store }}>
      {this.props.children}
    </ReactReduxContext.Provider>
  }
}
export default Provider

而 connect 方法,接收 mapStateToProps, mapDispatchToProps 兩個參數(shù),返回一個函數(shù),返回的函數(shù)接收 自定義組件(例如 Counter1 ),函數(shù)執(zhí)行后,返回最終的容器組件

// src/react-redux/connect.js
import React from 'react'
import {bindActionCreators} from 'redux'
import {ReactReduxContext} from './Context'

function connect(mapStateToProps, mapDispatchToProps) {
  return function (WrappedComponent) {
    // 返回最終的容器組件
     return class extends React.Component{
        static contextType = ReactReduxContext
        constructor(props, context){
          super(props)
          this.state = mapStateToProps(context.store.getState())
        }
        shouldComponentUpdate() {
          if (this.state === mapStateToProps(this.context.store.getState())) {
            return false;
          }
          return true;
        }
        componentDidMount () {
          this.unsubscribe = this.context.subscribe(() => {
            this.setState(mapStateToProps(this.context.store.getState()))
          })
        }
        componentWillUnmount (){
          this.unsubscribe && this.unsubscribe()
        }
        render(){
          const actions = bindActionCreators(
            mapDispatchToProps,
            this.context.store.dispatch
          )
          return <WrappedComponent {...this.state} {...this.props} {...actions}/>
       }
     }
   }
}

export default connect

可以看出,connect 方法中,有 bindActionCreators 綁定 action 與 store.dispatch, 有訂閱 store 中的 state 變化,這些都是我們只使用 redux ,需要在 react 組件中需要手動去寫的,幸運的是,現(xiàn)在 react-redux 幫我們?nèi)ジ闪?/p>

三、總結(jié)

通過上面的分享,我們終于知道,為什么 react 應(yīng)用中需要同時引入 redux 和 react-redux 了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • (一) 安裝 (二) 基礎(chǔ) (1) Action Actions 是把數(shù)據(jù)從應(yīng)用傳到 store 的有效載荷。它是...
    woow_wu7閱讀 553評論 0 0
  • redux學(xué)習(xí)筆記 學(xué)習(xí)動機 隨著 JavaScript 單頁應(yīng)用開發(fā)日趨復(fù)雜,JavaScript 需要管理比任...
    flura閱讀 424評論 0 0
  • Redux:一個純粹的狀態(tài)管理系統(tǒng),npm i redux -S redux是一個獨立的專門用于做狀態(tài)管理的JS庫...
    hellomyshadow閱讀 587評論 0 3
  • redux Redux 是 JavaScript 狀態(tài)容器,提供可預(yù)測化的狀態(tài)管理。 如果你的應(yīng)用有以下場景,可以...
    shadow123閱讀 752評論 0 0
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達很多信息。高興了當(dāng)然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,519評論 2 7

友情鏈接更多精彩內(nèi)容