Redux流程分析與實現

xiangzhihong發表於2019-03-04

#概述 隨著應用程式單頁面需求的越來越複雜,應用狀態的管理也變得越來越混亂,而Redux的就是為解決這一問題而出現的。在一個大型的應用程式中,應用的狀態不僅包括從伺服器獲取的資料,還包括本地建立的資料,以及反應本地UI狀態的資料,而Redux正是為解決這一複雜問題而存在的。

redux作為一種單向資料流的實現,配合react非常好用,尤其是在專案比較大,邏輯比較複雜的時候,單項資料流的思想能使資料的流向、變化都能得到清晰的控制,並且能很好的劃分業務邏輯和檢視邏輯。下圖是redux的基本運作的流程。

Redux流程分析與實現

如上圖所示,該圖展示了Redux框架資料的基本工作流程。簡單來說,首先由view dispatch攔截action,然後執行對應reducer並更新到store中,最終views會根據store資料的改變執行介面的重新整理渲染操作。

同時,作為一款應用狀態管理框架,為了讓應用的狀態管理不再錯綜複雜,使用Redux時應遵循三大基本原則,否則應用程式很容易出現難以察覺的問題。這三大原則包括: • 單一資料來源 整個應用的State被儲存在一個狀態樹中,且只存在於唯一的Store中。 • State是隻讀的 對於Redux來說,任何時候都不能直接修改state,唯一改變state的方法就是通過觸發action來間接的修改。而這一看似繁瑣的狀態修改方式實際上反映了Rudux狀態管理流程的核心思想,並因此保證了大型應用中狀態的有效管理。 • 應用狀態的改變通過純函式來完成 Redux使用純函式方式來執行狀態的修改,Action表明了修改狀態值的意圖,而真正執行狀態修改的則是Reducer。且Reducer必須是一個純函式,當Reducer接收到Action時,Action並不能直接修改State的值,而是通過建立一個新的狀態物件來返回修改的狀態。

redux的三大元素

和Flux框架不同,Redux框架主要由Action、Reducer和Store三大元素組成。

Action

Action是一個普通的JavaScript物件,其中的type屬性是必須的,用來表示Action的名稱,type一般被定義為普通的字串常量。為了方便管理,一般通過action creator來建立action,action creator是一個返回action的函式。

在Redux中,State的變化會導致View的變化,而State狀態的改變是通過接觸View來觸發具體的Action動作的,根據View觸發產生的Action動作的不同,就會產生不同的State結果。可以定義一個函式來生成不同的Action,這個函式就被稱為action creator。例如:

const ADD_TODO = '新增事件 TODO';

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

const action = addTodo('Learn Redux');

複製程式碼

上面程式碼中,addTodo就是一個action creator。但當應用程式的規模越來越大時,建議使用單獨的模組或檔案來存放action。

Reducer

當Store收到action以後,必須返回一個新的State才能觸發View的變化,State計算的過程即被稱為Reducer。Reducer本質上是一個函式,它接受Action和當前State作為引數,並返回一個新的State。格式如下:

const reducer = function (state, action) {
  // ...
  return new_state;
};

複製程式碼

為了保持reducer函式的純淨,請不要在reducer中執行如下的一些操作: • 修改傳入引數; • 執行有副作用的操作,如API請求和路由跳轉; • 呼叫非純函式,如 Date.now() 或 Math.random()

對於Reducer來說,整個應用的初始狀態就可以直接作為State的預設值。例如:

const defaultState = 0;
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};
//手動呼叫
const state = reducer(1, {
  type: 'ADD',
  payload: 2
});

複製程式碼

不過,在實際使用過程中,reducer函式並不需要像上面那樣進行手動呼叫,Store的store.dispatch方法會觸發Reducer的自動執行。為此,只需要在生成Store的時候將Reducer傳入createStore方法即可。例如:

import { createStore } from 'redux';
const store = createStore(reducer);

複製程式碼

在上面的程式碼中,createStore函式接受Reducer作為引數,該函式返回一個新的Store,以後每當store.dispatch傳送過來一個新的Action,就會自動呼叫Reducer得到新的State。

##Store Store就是資料儲存的地方,可以把它看成一個容器,整個應用中只能有一個Store。同時,Store還具有將Action和Reducers聯絡在一起的作用。Store具有以下的一些功能: • 維持應用的 state; • 提供getState()方法獲取state; • 提供dispatch(action)方法更新state; • 通過subscribe(listener)註冊監聽器; • 通過subscribe(listener)返回的函式登出監聽器。

根據已有的Reducer來建立Store是一件非常容易的事情,例如Redux提供的createStore函式可以很方便的建立一個新的Store。

import { createStore } from 'redux'
import todoApp from './reducers'
// 使用createStore函式建立Store
let store = createStore(todoApp)

複製程式碼

其中,createStore函式的第二個引數是可選的,該引數用於設定state的初始狀態。而這對於開發同構應用時非常有用的,可以讓伺服器端redux應用的state與客戶端的state保持一致,並用於本地資料初始化。

let store = createStore(todoApp, window.STATE_FROM_SERVER)
複製程式碼

Store物件包含所有資料,如果想得到某個時刻的資料,則需要利用state來獲取。例如:

import { createStore } from 'redux';
const store = createStore(fn);
//利用store.getState()獲取state
const state = store.getState();

複製程式碼

Redux規定,一個state只能對應一個view,只要state相同得到的view就相同,這也是Redux框架的重要特性之一。

到此,關於Redux的運作流程就非常的清晰了,下面總結下Redux的運作流程。

  1. 當使用者觸控介面時,呼叫store.dispatch(action)捕捉具體的action動作。
  2. 然後Redux的store自動呼叫reducer函式,store傳遞兩個引數給reducer函式:當前state和收到的action。其中,reducer函式必須是一個純函式,該函式會返回一個新的state。
  3. 根reducer會把多個子reducer的返回結果合併成最終的應用狀態,在這一過程中,可以使用Redux提供的combineReducers方法。使用combineReducers方法時,action會傳遞給每個子的reducer進行處理,在子reducer處理後會將結果返回給根reducer合併成最終的應用狀態。
  4. store呼叫store.subscribe(listener)監聽state的變化,state一旦發生改變就會觸發store的更新,最終view會根據store資料的更新重新整理介面。

Redux實現

1,建立store

store就是redux的一個資料中心,簡單的理解就是我們所有的資料都會存放在裡面,然後在介面上使用時,從中取出對應的資料。因此首先我們要建立一個這樣的store,可以通過redux提供的createStore方法來建立。

xport default function createStore(reducer, preloadedState, enhancer) {
  ...
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
複製程式碼

可以看到createStore有三個引數,返回一個物件,裡面有我們常用的方法,下面一一來看一下。

getState

getState用於獲取當前的狀態,格式如下:

function getState() {
    return currentState
  }
複製程式碼

Redux內部通過currentState變數儲存當前store,變數初始值即我們呼叫時傳進來的preloadedState,getState()就是返回這個變數。

subscribe

程式碼本身也不難,就是通過nextListeners陣列儲存所有的回撥函式,外部呼叫subscribe時,會將傳入的listener插入到nextListeners陣列中,並返回unsubscribe函式,通過此函式可以刪除nextListeners中對應的回撥。以下是該函式的具體實現:

var currentListeners = []
var nextListeners = currentListeners

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()        //生成一個新的陣列
    }
 }

function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    var isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      var index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
 }

複製程式碼

可以發現,上面的原始碼使用currentListeners和nextListeners兩個陣列來儲存,主要原因是在dispatch函式中會遍歷nextListeners,這時候可能會客戶可能會繼續呼叫subscribe插入listener,為了保證遍歷時nextListeners不變化,需要一個臨時的陣列儲存。

dispatch

當view dispatch一個action後,就會呼叫此action對應的reducer,下面是它的原始碼:

function dispatch(action) {  
  ...
  try {
      isDispatching = true
      currentState = currentReducer(currentState, action)  //呼叫reducer處理
    } finally {
      isDispatching = false
    }

    var listeners = currentListeners = nextListeners
    for (var i = 0; i < listeners.length; i++) {                   
      var listener = listeners[i]
      listener()
    }
  ...
}
複製程式碼

從上面的原始碼可以發現,dispatch函式在呼叫了currentReducer以後,遍歷nextListeners陣列,回撥所有通過subscribe註冊的函式,這樣在每次store資料更新,元件就能立即獲取到最新的資料。

replaceReducer

replaceReducer是切換當前的reducer,雖然程式碼只有幾行,但是在用到時功能非常強大,它能夠實現程式碼熱更新的功能,即在程式碼中根據不同的情況,對同一action呼叫不同的reducer,從而得到不同的資料。

function replaceReducer(nextReducer) {
 if (typeof nextReducer !== 'function') {
    throw new Error('Expected the nextReducer to be a function.')
  }

  currentReducer = nextReducer
  dispatch({ type: ActionTypes.REPLACE })
  }
複製程式碼

bindActionCreators

bindActionCreators方法的目的就是簡化action的分發,我們在觸發一個action時,最基本的呼叫是dispatch(action(param))。這樣需要在每個呼叫的地方都寫dispatch,非常麻煩。bindActionCreators就是將action封裝了一層,返回一個封裝過的物件,此後我們要出發action時直接呼叫action(param)就可以了。

react-redux

redux作為一個通用的狀態管理庫,它不只針對react,還可以作用於其它的像vue等。因此react要想完美的應用redux,還需要封裝一層,react-redux就是此作用。react-redux庫相對簡單些,它提供了一個react元件Provider和一個方法connect。下面是react-redux最簡單的寫法:

import { Provider } from 'react-redux';     // 引入 react-redux

……
render(
    <Provider store={store}>
        <Sample />
    </Provider>,
    document.getElementById('app'),
);

複製程式碼

connect方法複雜點,它返回一個函式,此函式的功能是建立一個connect元件包在WrappedComponent元件外面,connect元件複製了WrappedComponent元件的所有屬性,並通過redux的subscribe方法註冊監聽,當store資料變化後,connect就會更新state,然後通過mapStateToProps方法選取需要的state,如果此部分state更新了,connect的render方法就會返回新的元件。

export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
  ...
  return function wrapWithConnect(WrappedComponent) {
    ...
  }
}
複製程式碼

本文不詳細介紹React-Redux,可以訪問下面的連結React-Redux簡介及應用

相關文章