老生常談之Flux與Redux思想

Redux是一個通用的前端狀態管理庫,它不只普遍應用於 React App,在 Wepy、Flutter 等框架中也隨處可見它的身影,可謂是一招鮮吃遍天,它同時深受喜歡函數式編程(Functional Programming)人們的追捧,今天我就來和你們聊一聊Redux的基本思想。html

Flux

Flux是Facebook用於構建客戶端Web應用程序的基本架構,咱們能夠將Flux看作一種應用程序中的數據流的設計模式,而Redux正是基於Flux的核心思想實現的一套解決方案,它也獲得了原做者的確定。前端

首先,在Flux中會有如下幾個角色的出現:react

  • Dispacher:調度器,接收到Action,並將它們發送給Store。
  • Action:動做消息,包含動做類型與動做描述。
  • Store:數據中心,持有應用程序的數據,並會響應Action消息。
  • View:應用視圖,可展現Store數據,並實時響應Store的更新。

從通信的角度還可將其視爲Action請求層 -> Dispatcher傳輸層 -> Store處理層 -> View視圖層git

單向數據流

Flux應用中的數據以單一方向流動:github

  1. 視圖產生動做消息,將動做傳遞給調度器。
  2. 調度器將動做消息發送給每個數據中心。
  3. 數據中心再將數據傳遞給視圖。

單一方向數據流還具備如下特色:npm

  • 集中化管理數據。常規應用可能會在視圖層的任何地方或回調進行數據狀態的修改與存儲,而在Flux架構中,全部數據都只放在Store中進行儲存與管理。
  • 可預測性。在雙向綁定或響應式編程中,當一個對象改變時,可能會致使另外一個對象發生改變,這樣會觸發屢次級聯更新。對於Flux架構來說,一次Action觸發,只能引發一次數據流循環,這使得數據更加可預測。
  • 方便追蹤變化。全部引發數據變化的緣由均可由Action進行描述,而Action只是一個純對象,所以十分易於序列化或查看。

Flux的工做流

從上面的章節中咱們大概知道了Flux中各個角色的職責,那如今咱們再結合着簡單的代碼示例講解一下他們是如何構成一整個工做流的: 編程

b6682c2d.png

上圖中有一個Action Creator的概念,其實他們就是用於輔助建立Action對象,並傳遞給Dispatcher:redux

function addTodo(desc) {
  const action = {
    type: 'ADD_TODO',
    payload: {
      id: Date.now(),
      done: false,
      desciption: desc
    }
  }
  dispatcher(action)
}
複製代碼

在這裏我仍是但願經過代碼的形式進行簡單的描述,會更直觀一點,首先初始化一個項目:設計模式

mkdir flux-demo && cd flux-demo
npm init -y && npm i react flux
touch index.js
複製代碼

而後,咱們建立一個Dispatcher對象,它的本質是Flux系統中的事件系統,用於觸發事件與響應回調,並且在Flux中僅會有一個全局的Dispatcher對象:數組

import { Dispatcher } from 'flux';

const TodoDispatcher = new Dispatcher();
複製代碼

接着,註冊一個Store,響應Action方法:

import { ReduceStore } from 'flux/utils';

class TodoStore extends ReduceStore {
  constructor() {
    super(TodoDispatcher);
  }

  getInitialState() {
    return [];
  }

  reduce(state, action) {
    switch (action.type) {
      case 'ADD_TODO':
        return state.concat(action.payload);

      default:
        return state;
    }
  }
}
const TodoStore = new TodoStore();
複製代碼

在Store的構造器中將TodoDispatcher傳遞給了父級構造器調用,實際上是在Dispatcher上調用register方法註冊了Store,將其做爲dispatch的回調方法,用於響應每個Action對象。

到了這裏幾乎已經完成了一個Flux示例,就剩下鏈接視圖了。當 Store 改變時,會觸發一個 Change 事件,通知視圖層進行更新操做,如下爲完整代碼:

const { Dispatcher } = require('flux');
const { ReduceStore } = require('flux/utils');

// Dispatcher
const TodoDispatcher = new Dispatcher();

// Action Types
const ADD_TODO = 'ADD_TODO';

// Action Creator
function addTodo(desc) {
  const action = {
    type: 'ADD_TODO',
    payload: {
      id: Date.now(),
      done: false,
      desciption: desc
    }
  };
  TodoDispatcher.dispatch(action);
}

// Store
class TodoStore extends ReduceStore {
  constructor() {
    super(TodoDispatcher);
  }

  getInitialState() {
    return [];
  }

  reduce(state, action) {
    switch (action.type) {
      case ADD_TODO:
        return state.concat(action.payload);

      default:
        return state;
    }
  }
}
const todoStore = new TodoStore();

console.log(todoStore.getState()); // []
addTodo('早晨起來,擁抱太陽');
console.log(todoStore.getState()); // [ { id: 1553392929453, done: false, desciption: '早晨起來,擁抱太陽' } ]
複製代碼

Flux與React

Flux 這樣的架構設計其實在很早以前就出現了,可是爲何近幾年才盛行呢?我認爲很大一部分因素取決於 React 框架的出現,正是由於 React 的 Virtual DOM 讓數據驅動成爲了主流,再加上高效率的React diff,使得這樣的架構存在更加合理:

a837658f.png

在靠近視圖的頂層結構中,有一個特殊的視圖層,在這裏咱們稱爲視圖控制器( View Controller ),它用於從Store中獲取數據並將數據傳遞給視圖層及其後代,並負責監聽Store中的數據改變事件。

當接受到事件時,首先視圖控制器會從Store獲取最新的數據,並調用自身的setStateforceUpdate函數,這些函數會觸發View的render與全部後代的re-render方法。

一般咱們會將整個Store對象傳遞到View鏈的頂層,再由View的父節點依次傳遞給後代所須要的Store數據,這樣能保證後代的組件更加的函數化,減小了Controller-View的個數也意味着使更好的性能。

Redux

Redux是JavaScript應用可預測的狀態管理容器,它具備如下特性:

  • 可預測性,使用Redux能幫助你編寫在不一樣的環境中編寫行爲一致、便於測試的程序。
  • 集中性,集中化應用程序的狀態管理能夠很方便的實現撤銷、恢復、狀態持久化等。
  • 可調式,Redux Devtools提供了強大的狀態追蹤功能,能很方便的作一個時間旅行者。
  • 靈活,Redux適用於任何UI層,並有一個龐大的生態系統。

它還有三大原則:

  • 單一數據源。整個應用的State儲存在單個Store的對象樹中。
  • State狀態是隻讀的。您不該該直接修改State,而是經過觸發Action來修改它。Action是一個普通對象,所以它能夠被打印、序列化與儲存。
  • 使用純函數進行修改狀態。爲了指定State如何經過Action操做進行轉換,須要編寫reducers純函數來進行處理。reducers經過當前的狀態樹與動做進行計算,每次都會返回一個新的狀態對象。

與Flux的不一樣之處

123

Redux受到了Flux架構的啓發,但在實現上有一些不一樣:

  • Redux並無 dispatcher。它依賴純函數來替代事件處理器,也不須要額外的實體來管理它們。Flux嚐嚐被表述爲:(state, action) => state,而純函數也是實現了這一思想。
  • Redux爲不可變數據集。在每次Action請求觸發之後,Redux都會生成一個新的對象來更新State,而不是在當前狀態上進行更改。
  • Redux有且只有一個Store對象。它的Store儲存了整個應用程序的State。

Action

在Redux中,Action 是一個純粹的 JavaScript 對象,用於描述Store 的數據變動信息,它們也是 Store 的信息來源,簡單來講,全部數據變化都來源於 Actions 。

在 Action 對象中,必須有一個字段type用於描述操做類型,他們的值爲字符串類型,一般我會將全部 Action 的 type 類型存放於同一個文件中,便於維護(小項目能夠沒必要這樣作):

// store/mutation-types.js
export const ADD_TODO = 'ADD_TODO'
export const REMOVE_TODO = 'REMOVE_TODO'

// store/actions.js
import * as types from './mutation-types.js'

export function addItem(item) {
  return {
    type: types.ADD_TODO,
    // .. pass item
  }
}
複製代碼

Action對象除了type之外,理論上其餘的數據結構均可由本身自定義,在這裏推薦flux-standard-action這個Flux Action標準,簡單來講它規範了基本的Action對象結構信息:

{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.'
  }
}
複製代碼

還有用於表示錯誤的Action:

{
  type: 'ADD_TODO',
  payload: new Error(),
  error: true
}
複製代碼

在構造 Action 時,咱們須要使 Action 對象儘量攜帶更少的數據信息,好比能夠經過傳遞 id 的方式取代整個對象。

Action Creator

咱們將Action Creator與Action進行區分,避免混爲一談。在Redux中,Action Creator是用於建立動做的函數,它會返回一個Action對象:

function addTodo(text) {
  return {
    type: 'ADD_TODO',
    payload: {
      text,
    }
  }
}
複製代碼

Flux所不一樣的是,在Flux 中Action Creator 同時會負責觸發 dispatch 操做,而Redux只負責建立Action,實際的派發操做由store.dispatch方法執行:store.dispatch(addTodo('something')),這使得Action Creator的行爲更簡單也便於測試。

bindActionCreators

一般咱們不會直接使用store.dispatch方法派發 Action,而是使用connect方法獲取dispatch派發器,並使用bindActionCreators將Action Creators自動綁定到dispatch函數中:

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    { addTodo },
    dispatch
  );
}

const Todo = ({ addTodo }) => {}
export default connect(null, mapDispatchToProps)(Todo);
複製代碼

經過bindActionCreators以後,咱們能夠將這些Action Creators傳遞給子組件,子組件不須要去獲取dispatch方法,而是直接調用該方法便可觸發Action。

Reducers

對於Action來說,它們只是描述了發生了什麼事情,而應用程序狀態的變化,全由Reducers進行操做更改。

在實現Reducer函數以前,首先須要定義應用程序中State的數據結構,它被儲存爲一個單獨的對象中,所以在設計它的時候,儘可能從全局思惟去考慮,並將其從邏輯上劃分爲不一樣的模塊,採用最小化、避免嵌套,並將數據與UI狀態分別存儲。

Reducer是一個純函數,它會結合先前的state狀態與Action對象來生成的新的應用程序狀態樹:

(previousState, action) => newState
複製代碼

內部通常經過switch...case語句來處理不一樣的Action。

保持Reducer的純函數特性很是重要,Reducer須要作到如下幾點:

  • 不該該直接改變原有的State,而是在原有的State基礎上生成一個新的State。
  • 調用時不該該產生任何反作用,如API調用、路由跳轉等。
  • 當傳遞相同的參數時,每次調用的返回結果應該是一致的,因此也要避免使用Date.now()Math.random()這樣的非純函數。
combineReducers

Redux應用程序最多見的State形狀是一個普通的Javascript對象,其中包含每一個頂級鍵的特定於域的數據的「切片」,每一個「切片」都具備一個相同結構的reducer函數處理該域的數據更新,多個reducer也可同時響應同一個action,在須要的狀況獨立更新他們的state。

正是由於這種模式很常見,Redux就提供了一個工具方法去實現這樣的行爲:combineReducers。它只是用於簡化編寫Redux reducers最多見的示例,並規避一些常見的問題。它還有一個特性,當一個Action產生時,它會執行每個切片的reducer,爲切片提供更新狀態的機會。而傳統的單一Reducer沒法作到這一點,所以在根Reducer下只可能執行一次該函數。

Reducer函數會做爲createStore的第一個參數,而且在第一次調用reducer時,state參數爲undefined,所以咱們也須要有初始化State的方法。舉一個示例:

const initialState = { count: 0 }

functino reducer(state = initialState, action) {
  switch (action.type) {
    case: 'INCREMENT':
      return { count: state.count + 1 }
    case: 'DECREMENT':
      return { count: state.count - 1 }
    default:
      return state;
  }
}
複製代碼

對於常規應用來說,State中會儲存各類各樣的狀態,從而會形成單一Reducer函數很快變得難以維護:

...
  case: 'LOADING':
    ...
  case: 'UI_DISPLAY':
    ...
  ...
複製代碼

所以咱們的核心目標是將函數拆分得儘量短並知足單一職責原則,這樣不只易於維護,還方便進行擴展,接下來咱們來看一個簡單的TODO示例:

const initialState = {
  visibilityFilter: 'SHOW_ALL',
  todos: []
}

function appReducer(state = initialState, action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER': {
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    }
    case 'ADD_TODO': {
      return Object.assign({}, state, {
        todos: state.todos.concat({
          id: action.id,
          text: action.text,
          completed: false
        })
      })
    }
    default:
      return state
  }
}
複製代碼

這個函數內包含了兩個獨立的邏輯:過濾字段的設置與TODO對象操做邏輯,若是繼續擴展下去會使得Reducer函數愈來愈龐大,所以咱們須要將這兩個邏輯拆分開進行單獨維護:

function appReducer(state = initialState, action) {
  return {
    todos: todosReducer(state.todos, action),
    visibilityFilter: visibilityReducer(state.visibilityFilter, action)
  }
}

function todosReducer(todosState = [], action) {
  switch (action.type) {
    case 'ADD_TODO': {
      return Object.assign({}, state, {
        todos: state.todos.concat({
          id: action.id,
          text: action.text,
          completed: false
        })
      })
    }
    default:
      return todosState
  }
}

function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return setVisibilityFilter(visibilityState, action)
    default:
      return visibilityState
  }
}
複製代碼

咱們將整個Reducer對象拆爲兩部分,而且他們獨自維護本身部分的狀態,這樣的設計模式使得整個Reducer分散爲獨立的切片。Redux內置了一個combineReducers工具函數,鼓勵咱們這樣去切分頂層Reducer,它會將全部切片組織成爲一個新的Reducer函數:

const rootReducer = combineReducers({
  todos: todosReducer,
  visibilityFilter: visibilityReducer
})
複製代碼

在 combineReducers 返回的state對象中,每一個鍵名都表明着傳入時子Reducer的鍵名,他們做爲子Reducer中 State 的命名空間。

Store

在Redux應用中只有一個單一的store,經過createStore進行建立。Store對象用於將Actions與Reducers結合在一塊兒,它具備有如下職責:

  • 儲存應用的State,並容許經過getState()方法訪問State。
  • 提供dispatch(action)方法將Action派發到Reducer函數,以此來更新State。
  • 經過subscribe(listener)監聽狀態更改。

對於subscribe來說,每次調用dispatch方法後都會被觸發,此時狀態樹的某一部分可能發生了改變,咱們能夠在訂閱方法的回調函數裏使用getStatedispatch方法,但須要謹慎使用。subscribe在調用後還會返回一個函數unsubscribe函數用於取消訂閱。

Redux Middleware

對於中間件的概念相信你們經過其餘應用有必定的概念瞭解,對於Redux來說,當咱們在談論中間件時,每每指的是從一個Action發起直到它到達Reducer以前的這一段時間裏所作的事情,Redux經過Middleware機制提供給三方程序擴展的能力。

爲了更好的說明中間件,我先用Redux初始化一個最簡實例:

const { createStore } = require('redux');

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

function reducer(state = 0, action) {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      throw new Error('decrement error'); 
    default:
      return state;
  }
}

void function main() {
  const store = createStore(reducer);
  store.dispatch({ type: INCREMENT });
  console.log(store.getState()); // 打印 1
}()

複製代碼

Step 1. 手動添加打印日誌的中間件

爲了深入的理解Redux中間件,咱們一步步去實現具備中間件功能的函數。爲了追蹤程序的狀態變化,可能咱們須要實現一個日誌打印中間件機制,用於打印Action與執行後的State變化。咱們首先經過store對象建立一個logger對象,在dispatch的先後進行日誌打印:

void (function main() {
  const store = createStore(reducer);
  const logger = loggerMiddleware(store);
  logger({ type: INCREMENT });

  function loggerMiddleware(store) {
    return action => {
      console.log('dispatching', action);
      let result = store.dispatch(action);
      console.log('next state', store.getState());
      return result;
    };
  }
})();

// 程序運行結果
dispatching { type: 'INCREMENT' }
next state 1
複製代碼

Step 2. 再添加一個錯誤打印的中間件

爲了監控應用程序的狀態,咱們還須要實現一箇中間件,當在應用程序dispatch過程當中發生錯誤時,中間件能及時捕獲錯誤並上報(一般可上報至Sentry,但在這裏就簡單打印錯誤了):

void (function main() {
  const store = createStore(reducer);
  const crasher = crashMiddleware(store);
  crasher({ type: DECREMENT });

  function crashMiddleware(store) {
    return action => {
      try {
        return dispatch(action);
      } catch (err) {
        console.error('Caught an exception!', err);
      }
    };
  }
})();
複製代碼

執行程序後,可在命令行內看到函數正確的捕獲DECREMENT中的錯誤

Caught an exception! ReferenceError: dispatch is not defined
複製代碼

Step 3. 將2箇中間件串聯在一塊兒

在應用程序中通常都會有多箇中間件,而將不一樣的中間件串聯在一塊兒是十分關鍵的一步操做,若你讀過Koa2的源碼,你大概瞭解一種被稱之爲compose的函數,它將負責處理中間件的級聯工做。

在這裏,爲了理解其原理,咱們仍是一步一步進行分析。前面兩個中間件的核心目標在於將Dispatch方法進行了一層包裝,這樣來講,咱們只須要將dispatch一層層進行包裹,並傳入最深層的中間件進行調用,便可知足咱們程序的要求:

dispatch = store.dispatch

↓↓↓

// 沒有中間件的狀況
dispatch(action)

↓↓↓

// 當添加上LoggerMiddleware
LoggerDispatch = action => {
  // LoggerMiddleware TODO
  dispatch(action)
  // LoggerMiddleware TODO
}
dispatch(action)

↓↓↓

// 當添加上CrashMiddleware
CrashDispatch = action => {
  // CrashMiddleware TODO
  LoggerDispatch(action)
  // CrashMiddleware TODO
}

複製代碼

若是你熟悉使用高階函數,相信上述思路並不難以理解,那讓咱們經過修改源代碼,嘗試一下經過這樣的方式,是否能使兩個中間件正常工做:

void function main() {
  const store = createStore(reducer);
  let dispatch = store.dispatch
  dispatch = loggerMiddleware(store)(dispatch)
  dispatch = crashMiddleware(store)(dispatch)
  dispatch({ type: INCREMENT });
  dispatch({ type: DECREMENT });

  function loggerMiddleware(store) {
    return dispatch => {
      return action => {
        console.log('dispatching', action);
        let result = dispatch(action);
        console.log('next state', store.getState());
        return result;
      };
    };
  }

  function crashMiddleware(store) {
    return dispatch => {
      return action => {
        try {
          return dispatch(action);
        } catch (err) {
          console.error('Caught an exception!', err);
        }
      };
    };
  }
}();
複製代碼

此時打印結果爲(符合預期):

dispatching { type: 'INCREMENT' }
next state 1
dispatching { type: 'DECREMENT' }
Caught an exception! Error: decrement error
複製代碼

固然,咱們但願以更優雅的方式生成與調用dispatch,我會指望在建立時,經過傳遞一箇中間件數組,以此來生成Store對象:

// 簡單實現
function createStoreWithMiddleware(reducer, middlewares) {
  const store = createStore(reducer);
  let dispatch = store.dispatch;
  middlewares.forEach(middleware => {
    dispatch = middleware(store)(dispatch);
  });
  return Object.assign({}, store, { dispatch });
}


void function main() {
  const middlewares = [loggerMiddleware, crashMiddleware];
  const store = createStoreWithMiddleware(reducer, middlewares);
  store.dispatch({ type: INCREMENT });
  store.dispatch({ type: DECREMENT });
  // ...
}()
複製代碼

Step 4. back to Redux

經過Step 1 ~ 3 的探索,咱們大概是照瓢畫葫實現了Redux的中間件機制,如今讓咱們來看看Redux自己提供的中間件接口。

createStore方法中,支持一個enhancer參數,意味着三方擴展,目前支持的擴展僅爲經過applyMiddleware方法建立的中間件。

applyMiddleware支持傳入多個符合Redux middleware API的Middleware,每一個Middleware的形式爲:({ dispatch, getState }) => next => action。讓咱們稍做修改,經過applyMiddleware與createStore接口實現(只須要修改建立store的步驟):

// ...
  const middlewares = [loggerMiddleware, crashMiddleware];
  const store = createStore(reducer, applyMiddleware(...middlewares));
  // ...
複製代碼

經過applyMiddleware方法,將多個 middleware 組合到一塊兒使用,造成 middleware 鏈。其中,每一個 middleware 都不須要關心鏈中它先後的 middleware 的任何信息。 Middleware最多見的場景是實現異步actions方法,如redux-thunkredux-saga

異步Action

對於一個標準的Redux應用程序來講,咱們只能簡單的經過派發Action執行同步更新,爲了達到異步派發的能力,官方的標準作法是使用 redux-thunk 中間件。

爲了明白什麼是 redux-thunk ,先回想一下上文介紹的Middleware API:({ dispatch, getState }) => next => action,藉由靈活的中間件機制,它提供給 redux-thunk 延遲派發Action的能力,容許了人們在編寫Action Creator時,能夠不用立刻返回一個Action對象,而是返回一個函數進行異步調度,因而稱之爲Async Action Creator

// synchronous, Action Creator
function increment() {
	return {
    type: 'INCREMENT'
	}
}

// asynchronous, Async Action Creator
function incrementAsync() {
  return dispatch => {
    setTimeout(() => dispatch({ type: 'INCREMENT' }), 1000)
  }
}
複製代碼

而 redux-thunk 源碼也不過10行左右:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
複製代碼

經過dispatch(ActionCreator())進行調用時,函數會判斷參數的類型:

  1. 若爲對象,走正常的觸發流程,直接派發Action。
  2. 若爲函數,則將其視爲Async Action Creator,將dispatch方法與getState方法做爲參數注入,若是全局註冊了withExtraArgument的話也會做爲第三個參數進行傳入。

至於爲何稱其爲"thunk",它是來源於"think",i變爲了u,意味着將絕對權從我轉交給你,這是我認爲較好的解釋。若是要溯源的話,其實這是一種「求值策略」的模式,即函數參數到底應該什麼時候求值,好比一個函數:

function test(y) { return y + 1 }
const x = 1;
test(x + 1);
複製代碼

這時人們有兩種爭論點:

  • 傳值調用,即在進入函數體以前,就計算x + 1 = 2,再將值傳入函數;
  • 傳名調用,即直接將表達式x + 1傳入函數,須要用到時再計算表達式的值。

而一般編譯器的「傳名調用」的實現,每每是將參數放到一個臨時函數中,再將臨時函數傳入函數體內,而這個函數就被稱之爲 Thunk ,若採起傳名調用,上面的函數調用會轉化爲 Thunk 傳參形式:

const thunk = () => (x + 1)
function test(thunk) {
  return thunk() + 1;
}
複製代碼

參考資料

相關文章
相關標籤/搜索