學習在React項目中使用Redux

Redux是用來管理JavaScript應用的狀態的。官方文檔地址:Redux。本文講述如何在React中使用Redux,重點偏向Redux。若是須要了解React,能夠參考React官網或者React入門前一陣入門React時寫的文|ω・))。javascript

Redux簡介

  1. Redux是什麼?java

    Redux is a predictable state container for JavaScript apps.react

    Redux是一個用在JavaScript應用中的可預測狀態容器(這裏的狀態指的是存放信息的對象)。Redux是用來對狀態進行統一管理的,整個應用的狀態以一棵對象樹的方式被存放在store中。狀態是隻讀的,惟一改變狀態的方式是dispatch actions,因此狀態的改變是可預測的。經過dispatch改變store中的state,經過subscribe監聽狀態的改變,經過getState來拿到當前的狀態,這樣數據的處理都集中在Redux中。git

  2. 爲何要用Redux?github

    想象這樣的場景,組件一、組件二、組件三、組件五、組件7(下圖標綠的組件)中都須要使用一些數據,由於在React中,數據是單向流動的,因此在組件7中改變數據了,就須要組件7通知組件4改變數據,組件4再通知組件1改變數。在組件1中改變數據以後再層層往下傳遞數據。這還只是下圖這樣結構比較簡單的應用,若是應用更復雜,那處理起來就是一團亂麻了。因此咱們須要用Redux來對數據(狀態)進行統一的管理,當須要改變某數據時,直接改變store中的數據,當要獲取數據時,也直接從store中拿數據。json

    圖-1 沒使用Redux時的處理流程
    圖-2 使用Redux時的處理流程
  3. React和Redux有什麼關係?redux

    聽見Redux這個名字的時候,由於和React同樣都是以Re開頭的,可能會產生Redux是專門爲React解決問題的一個工具這樣的誤解,但實際上React和Redux啥關係也沒有,是兩個獨立的工具。你能夠將Redux和React結合起來使用,也能夠將Redux和jQuery,甚至Redux和JavaScript結合使用。app

Redux

Redux中有幾個必須知道的概念:action、reducer、store。下文中提到的state(狀態)指的是存放信息的對象,這個對象是放在Redux的store中的。dom

actions

actions是從應用到store的數據的載體。當咱們須要改變store中的數據的時候,須要dispatch actions。action必須有一個type屬性,代表即將執行的action的類型,type一般被定義爲一個字符常量。action對象中的其餘屬性是攜帶的信息。異步

例如:

{
    type: ADD_NUMBER,
    number: 1
}
複製代碼

action creator是建立action的函數,簡單地返回action,它能根據傳入的參數設置action的屬性值。

例如:

const ADD_NUMBER = 'ADD_NUMBER'
export const addNumber = number => ({
  type: ADD_NUMBER,
  number
})
複製代碼

reducers

reducer是純函數,它是用來指明當action發送(dispatch)到store的時候,state是如何改變的。它的第一個參數爲前一個state,第二個參數爲action對象,返回的值是下一個state。

純函數:

  1. 傳入相同的參數會返回相同的結果。
  2. 執行純函數不會形成反作用。(這裏的反作用指的是函數改變了做用域以外的狀態值,好比函數外部定義了變量a,在函數內部改變了變量a的值。)

在reducer中不能作如下操做:改變它的參數;形成反作用;調用非純函數(好比Math.random())。

如下的caculate函數是一個簡單的reducer。若是createStore函數沒有傳入第二個參數(state的初始值),那麼首次調用caculate的時候,實參state的值爲undefined。因此給state參數設置了一個默認值0。

function caculate (state = 0, action) {
  const { number } = action
  switch (action.type) {
    case 'ADD_NUMBER':
      return state + number
    case 'SUBTRACT_NUMBER':
      return state - number
    case 'MULTIPLY_NUMBER':
      return state * number
    default: 
      return state
  }
}
複製代碼

當應用比較大的時候,通常的作法是使用多個小的reducers,每一個reducer處理特定的內容。而後使用Redux提供的combineReducers將reducers合併爲一個reducer,合併後的reducer做爲createStore的參數來建立store實例。下文介紹完store以後,會給出一個簡單的例子來講明這部份內容。

store

store將actions和reducers結合起來,store的功能是:

  1. 保存應用的state。
  2. 經過getState()拿到state。
  3. 經過dispatch(action)更新state。
  4. 經過subscribe(listener)註冊監聽器。listener是一個函數,當發送action的時候會執行listener。
  5. 執行subscribe(listener)會返回 一個函數,調用這個函數可以註銷監聽器。

一個應用中只能有一個store,想要拆分數據的話,須要使用多個reducers。

如下代碼中的caculate是上文建立好的reducer,addNumber是上文定義的action creator。

import { createStore } from 'redux'

const store = createStore(caculate)
const listener = () => console.log(store.getState()) // 打印當前狀態
const unsubscribe = store.subscribe(listener)

store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))

unsubscribe()
複製代碼

使用store.subscribe(listener)註冊了監聽器,當store.dispatch()發送action到store,reducer將state改變以後,監聽器函數listener會執行,打印出如下結果:

1
3
6
複製代碼

我須要另外一個reducer來處理標籤切換,如下是相應的action creatore和reducer。

const SELECT_TAB = 'SELECT_TAB'
const selectTab = tab => ({
  type: SELECT_TAB,
  tab
}) 
function switchTab (state = 'CACULATE', action) {
  switch (action.type) {
    case 'SELECT_TAB':
      return action.tab
    default:
      return state
  }
}
複製代碼

如今就有了兩個reducer。使用combineReducers將reducers合併爲一個根reducer,將根reducer做爲createStore的第一個參數,初始的state值做爲createStore的第二個參數來建立store。

let state = {
  caculate: 0,
  switchTab: 'CACULATE'
}
const reducer = combineReducers({ caculate, switchTab })
const store = createStore(reducer, state)
const listener = () => console.log(store.getState())
const unsubscribe = store.subscribe(listener)

store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))

unsubscribe()
複製代碼

combineReducers所作的事情是生成一個函數,調用這個函數時,會執行每一個reducer,並將執行的結果從新整合爲一個對象,做爲新的state。當調用store.dispatch(addNumber(1))時,兩個reducer(caculate和switchTab)都會被調用,它們執行的結果會組成一個新的state樹。

代碼段中的:

const reducer = combineReducers({ caculate, switchTab })
複製代碼

和如下代碼段的效果是同樣的,

function reducer (state = {}, action) {
  return {
    caculate: caculate(state.caculate, action),
    switchTab: switchTab(state.switchTab, action)
  }
}
複製代碼

只是combineReducers會有更多的處理操做。

在React項目中使用Redux

react-redux插件是專門用於在React中使用redux的,react-redux官網地址

React Redux將展現組件(presentational components)和容器組件(container components)分離了。

Redux官網中有二者的對比表格:

Presentational Components Container Components
Purpose How things look (markup, styles) How things work (data fetching, state updates)
Aware of Redux No Yes
To read data Read data from props Subscribe to Redux state
To change data Invoke callbacks from props Dispatch Redux actions
Are written By hand Usually generated by React Redux

容器組件能將展現組件和Redux聯繫起來。通常使用React Redux提供的connect方法生成容器組件。

React Redux

  1. connect 方法

    調用connect方法會返回一個包裝函數,這個函數接收組件並返回一個新的包裝組件。這裏的包裝組件便是上文說到的容器組件,容器組件能將展現組件和Redux聯繫起來。

    connect的使用方法是:

    connect(mapStateToProps? mapDispatchToProps?, mergeProps?, options?)(組件)
    複製代碼
    • mapStateToProps,這個參數是一個函數,當store更新的時候,mapStateToProps就會被調用,若是不想訂閱store的更新,就在這個位置傳null或undefined。從函數名可知,這個參數的做用是將Redux store的state映射到React組件的屬性。mapStateToProps函數的格式是這樣的:(state, ownProps?) => Object,state指Redux store中的state,第二個參數是組件本身的屬性。這個函數執行後返回的對象會和組件的屬性合併。
    • mapDispatchToProps,這個參數能夠是一個對象或者函數。從函數名可知,這個參數的做用是將Redux store的dispatch映射到React組件的屬性,若是沒有傳入這個參數,那麼組件會默認收到dispatch屬性,這個dispatch屬性的值是Redux store的dispatch方法。若是傳入的參數是一個函數,那麼這個函數是這樣的:(dispatch, ownProps?) => Object,它的第一個參數是Redux中的dispatch,第二個參數是組件本身的屬性。這個函數返回一個對象,這個對象的每一個屬性值都是一個函數,這個對象會合併到組件的屬性中。
  2. Provider組件

    全部的容器組件都須要可以訪問Redux的store,若是不使用Provider組件,就只能在全部容器組件的屬性中傳入store,假如在組件樹中深層嵌套了容器組件,可能有的展現組件也須要傳入store屬性。可是使用Provider組件包裹上應用根組件後,應用中的全部容器組件就都能訪問到Redux的store了。

舉個例子,定義容器組件的時候這樣寫:

import { connect } from 'react-redux'
import { addNumber, subtractNumber, multiplyNumber } from '../actions'
import Caculation from '../components/Caculation'

const mapStateToProps = state => ({
  caculate: state.caculate
})

const mapDispatchToProps = dispatch => ({
  plus: number => dispatch(addNumber(number)),
  subtract: number => dispatch(subtractNumber(number)),
  multiply: number => dispatch(multiplyNumber(number))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Caculation)
複製代碼

那麼展現組件Caculation的屬性中就會有caculate,plus,subtract,multiply這幾個屬性,其中caculate是state.caculate;plus,subtract,multiply是三個函數,調用函數的時候能進行dispatch操做。

異步獲取信息

Redux只支持同步數據流,若是想要使用異步actions,須要使用applyMiddleware加強createStore。使用redux-thunk中間件可以發起同步或異步的actions,而且能夠設計知足某種條件才dispatch actions。使用Redux Thunk以後,action creator能夠返回一個函數,這個函數的第一個參數是dispatch,第二個參數是getState。 首先下載好redux-thunk,並使用applyMiddleware加強createStore:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
...

const store = createStore(rootReducer, applyMiddleware(thunk))
複製代碼

在thunk中間件的幫助下,就能夠像下面這樣使用異步action 了。在請求開始和請求結束的地方都觸發一個action,在請求開始的地方觸發的action能夠用來展現一個加載中的loading圖等。這裏只簡單地在請求開始時觸發了action,沒有作後續的使用。

const requestLeaderBoard = () => ({
  type: REQUEST_LEADERBOARD
})

const receiveLeaderBoard = (json) => ({
  type: RECEIVE_LEADERBOARD,
  leaderboard: json.data
})

export function fetchLeaderBoard () {
  return dispatch => {
    dispatch(requestLeaderBoard())
    return fetch(`這裏用一個get請求的地址`)
      .then(res => res.json())
      .then(json => dispatch(receiveLeaderBoard(json)))
  }
}
複製代碼

爲了學習redux寫了一個練習項目:源碼地址

遇到的問題

  1. 定義的reducer執行了「多餘」的次數。

    • 狀況1:沒有使用combineReducers時,reducer會多執行1次,action的type爲:@@redux/INITd.c.p.m.e.7
    • 狀況2:使用combineReducers以後,每一個reducer都多執行了3次,action的type分別是:@@redux/INITf.1.c.7.u.x@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9@@redux/INITf.1.c.7.u.x。有3個多出的reducer調用。

    狀況2的問題請教了可愛的同事楊老闆,楊老闆找到了相應的源碼給出瞭解答,而後我又按照相同的方式在源碼中找了下,獲得了狀況1的答案。

    (1)狀況1出現的緣由:

    當使用createStore建立好一個store以後,Redux會dispatch一個初始化action(以下),以保證每一個reducer返回它們的默認state。

    dispatch({ type: ActionTypes.INIT })
    複製代碼

    (2)狀況2出現的緣由:

    執行第一個action@@redux/INITf.1.c.7.u.x是由於Redux在執行combineReducers的過程當中調用了一遍reducer,做用是檢查調用reducer返回的初始state是否是undefined,若是是undefined會拋出一個錯誤。

    const initialState = reducer(undefined, { type: ActionTypes.INIT })
    複製代碼

    第二個action@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9也是在combineReducers執行的過程當中進行處理的,目的是檢查action的名稱和Redux內私有的名稱是否重複,若是重複,就拋出一個錯誤。

    第三個action和狀況1同。

相關文章
相關標籤/搜索