為什麼redux要返回一個新的state引發的血案

彭道寬發表於2018-12-20

Redux的內幕(一)

一個問題引發的血案

博主在面試的過程中,面試官問 : “看你簡歷,Vue和React都使用過,你能說一下 Vue和React的區別嘛?”, 然後吧唧吧唧說了一下,於是!血案發生了,當我答道Vuex和Redux的時候,面試官問了一句,為什麼Redux總是要返回一個新的 state ?返回舊的 state 為什麼不行 ?面試結果不用說,GG了。

重振旗鼓

過了大半個月,自己總結面試經驗的時候,把Redux的原始碼看了一遍,ojbk,看的暈頭轉向,然後去github上看了一些大哥們的解讀,再自己總結一哈,寫個專欄,用於自己以後的複習

基礎瞭解

Redux是什麼?

Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理方案, 官網裡是這麼介紹的 :

  // Redux is a predictable state container for JavaScript apps.
複製程式碼

那麼它能做什麼?

  // It helps you write applications that behave consistently, run in different environments (client, server, and native) and are easy to test.
  // On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
複製程式碼

三大原則

  • 單一資料來源 : 整個應用的 state 都儲存在一顆 state tree 中,並且只存在與唯一一個 store 中

  • state 是隻讀的 : 唯一改變 state 的方法只能通過觸發 action,然後通過 action 的 type 進而分發 dispatch 。不能直接改變應用的狀態

  • 狀態修改均由純函式完成 : 為了描述 action 如何改變 state tree,需要編寫 reducers

修修邊幅

這裡我們先來了解一下store、middleware、action、reducer等知識

store

這裡的 store 是由 Redux 提供的 createStore(reducers, preloadedState, enhancer) 方法生成。從函式簽名看出,要想生成 store,必須要傳入 reducers,同時也可以傳入第二個可選引數初始化狀態(preloadedState)。第三個引數一般為中介軟體 applyMiddleware(thunkMiddleware),看看程式碼,比較直觀

  import { createStore, applyMiddleware } from 'redux'
  import thunkMiddleware from 'redux-thunk' // 這裡用到了redux-thunk

  const store = createStore(
    reducers,
    state,
    applyMiddleware(thunkMiddleware) // applyMiddleware首先接收thunkMiddleware作為引數,兩者組合成為一個新的函式(enhance)
  )
複製程式碼

redux 中最核心的 API 就是 —— createStore, 通過 createStore 方法建立的store是一個物件,它本身包含4個方法 :

  • getState() : 獲取 store 中當前的狀態。

  • dispatch(action) : 分發一個 action,並返回這個 action,這是唯一能改變 store 中資料的 方式。

  • subscribe(listener) : 註冊一個監聽者,它在 store 發生變化時被呼叫。

  • replaceReducer(nextReducer) : 更新當前 store 裡的 reducer,一般只會在開發模式中呼叫該方法。

middleware

下圖中表達的是Redux 中一個簡單的同步資料流動場景,點選 button 後,在回撥中分發一個 action, reducer 收到 action 後,更新 state 並通知 view 重新渲染。

單向資料流,看著沒什麼問題。 但是,如果需要列印每一個 action 資訊來除錯,就得去改 dispatch 或者 reducer 實現,使其具有 列印日誌的功能。

又比如,點選 button 後,需要先去服務端請求資料,只有等資料返回後,才能重新渲染 view,此時我們希望 dispatch 或 reducer 擁有非同步請求的功能。再比如,需要非同步請求資料返回後,列印一條日誌,再請求資料,再列印日誌,再渲染。

面對多樣的業務場景,單純地修改 dispatch 或 reducer 的程式碼顯然不具有普適性,Redux 借鑑了 Node.js Koa 裡 middleware 的思想,Redux 中 reducer 更關心的是資料的轉化邏輯,所以 middleware 就是為了增強 dispatch 而出現的。

Action

  // 引用官網的介紹

  // Actions are payloads of information that send data from your application to your store. 
  // They are the only source of information for the store
  // You send them to the store using store.dispatch().
複製程式碼

Action 是把資料從應用傳到 store 的有效載荷。它是 store 資料的唯一來源。簡單來說,Action就是一種訊息型別,他告訴Redux是時候該做什麼了,並帶著相應的資料傳到Redux內部。

Action就是一個簡單的物件,其中必須要有一個type屬性,用來標誌動作型別(reducer以此判斷要執行的邏輯),其他屬性使用者可以自定義。如:

  const START_FETCH_API = 'START_FETCH_API'
複製程式碼
  {
    type: START_FETCH_API,
    data: {
      id: itemId,
      value: 'I am Value'
    }
  }
複製程式碼

Action Creator

看看官網中的介紹 : Action Creator are exactly that—functions that create actions. It's easy to conflate the terms “action” and “action creator”, so do your best to use the proper term。 也就是說 : Redux 中的 Action Creator 只是簡單的返回一個 Action

  function fetchStartRequestApi(jsondata) {
    return {
      type: START_FETCH_API,
      data: jsondata
    }
  }
複製程式碼

我們知道,Redux 由 Flux 演變而來,在傳統的 Flux 中, Action Creators 被呼叫之後經常會觸發一個dispatch。比如:

  function fetchStartRequestApiDispatch(jsondata) {
    const action = {
      type: START_FETCH_API,
      data: jsondata
    }
    dispatch(action)
  }
複製程式碼

但是,在Redux中,我們只需要把 Action Creators 返回的結果傳給 dispatch() ,就完成了發起一個dispatch 的過程,甚至於 建立一個 被繫結的 Action Creators 來自動 dispatch

  // example 1
  dispatch(fetchStartRequestApi(jsondata))

  // example 2
  const boundFetchStartRequestApiDispatch = jsondata => dispatch(fetchStartRequestApi(jsondata))
複製程式碼

這裡有人就要昏厥了,dispatch() 是個啥?其實前面就講過了,通過 createStore() 建立的 store 物件,他有一個方法 : dispatch(action),store 裡能直接通過 store.dispatch() 呼叫 dispatch() 方法,但是多數情況下,我們都會使用 react-redux 提供的 connect() 幫助器來呼叫。bindActionCreators() 可以自動把多個 action 建立函式繫結到 dispatch() 方法上。

Reducers

  // 引用官網的介紹

  // Reducers specify how the application's state changes in response to actions sent to the store. 
  // Remember that actions only describe what happened, but don't describe how the application's state changes
複製程式碼

上邊也說過了,Reducers必須是一個純函式,它根據action處理state的更新,如果沒有更新或遇到未知action,則返回舊state;否則返回一個新state物件。__注意:不能修改舊state,必須先拷貝一份state,再進行修改,也可以使用Object.assign函式生成新的state。__具體為什麼,我們讀原始碼的時候就知道啦~

永遠不要在 reducer 裡做這些操作:

  • 修改傳入引數;

  • 執行有副作用的操作,如 API 請求和路由跳轉;

  • 呼叫非純函式,如 Date.now() 或 Math.random();

下邊上個例子程式碼,幫助消化,傳送請求,獲取音樂列表

  // action.js

  /*
   * action 型別
  */  
  export const START_FETCH_API = 'START_FETCH_API'
  export const STOP_FETCH_API = 'STOP_FETCH_API'
  export const RECEIVE_DATA_LIST = 'RECEIVE_DATA_LIST'
  export const SET_OTHER_FILTERS = 'SET_OTHER_FILTERS'

  /*
   * 其它的常量
  */
  export const otherFilters = {
    SHOW_ALL: 'SHOW_ALL',
    SHOW_ACTIVE: 'SHOW_ACTIVE'
  }

  /*
   * action 建立函式
  */
  export function startFetchApi() {
    return {
      type: START_FETCH_API
    }
  }

  export function stopFetchApi() {
    return {
      type: STOP_FETCH_API
    }
  }

  export function receiveApi(jsondata) {
    return {
      type: RECEIVE_DATA_LIST,
      data: jsondata
    }
  }

  export function setOtherFilters (filter) {
    return {
      type: SET_OTHER_FILTERS,
      data: filter
    }
  }

  // 非同步
  export const fetchMusicListApi = (music_id) => dispatch => {
    dispatch(startFetchApi())
    fetch({
      url: url,
      method: 'POST',
      data: {
        music_id: music_id
      }
    }).then((res) => {
      dispatch(stopFetchApi())
      dispatch(receiveApi())
    }).catch((err) => {
      console.log(err)
    })
  }
複製程式碼
  // reducers.js
  
  // 引入 action.js
  import { otherFilters } from './action'

  // 初始 state
  const initialState = {
    otherFilters: otherFilters.SHOW_ALL,
    list: [],
    isFetching: false
  }

  function reducers(state, action) {
    switch(action.type) {
      case SET_OTHER_FILTERS: 
        return Object.assign({}, state, {
          otherFilters: action.payload.data
        })
      case START_FETCH_API:
        return Object.assign({}, state, {
          isFetching: true
        })
      case STOP_FETCH_API:
        return Object.assign({}, state, {
          isFetching: false
        })
      case RECEIVE_DATA_LIST:
        return Object.assign({}, state, {
          list: [ ...action.payload.data ]
        })
      default: 
        return state
    }
  }
複製程式碼

注意

  1. 不要修改 state。 使用 Object.assign() 新建了一個副本。不能這樣使用 Object.assign(state, { otherFilters: action.payload.data }),因為它會改變第一個引數的值。你必須把第一個引數設定為空物件

  2. 在 default 情況下返回舊的 state。遇到未知的 action 時,一定要返回舊的 state。

心中有疑惑

  • bindActionCreators() 是如何自動幫我把action繫結到dispatch上的?

  • 什麼是純函式?

  • 為什麼reducer必須是純函式?

  • 為什麼只能通過action來修改state,直接修改有什麼問題?

  • bindActionCreators() 是如何自動幫我把action繫結到dispatch上的?

  • 為什麼 reducers 在 default 的情況下,一定要返回舊的state?

  • ...

未待完續

還沒寫完,還有下文,只是想再瞭解深入一點,然後繼續寫總結,目的是給自己,包括新手們看的,望各位大佬,手下留情,各位大佬沒有興趣可以跳過~,直接去github上看原始碼哈,傳送門在這裡 : Redux


2019.01.03 更新,下文在這 : 《為什麼 redux 要返回一個新的 state 引發的血案(二)》

相關連結

相關文章