深入理解redux

lucifer210發表於2018-03-06

原文刊登在我的github

為什麼寫這篇文章

業餘時間我也算看了不少優秀開源專案的原始碼比如react,redux,vuex,vue,babel,ant-design等,但是很少系統地進行總結,學到的知識非常有限,因此我一直想寫一篇完善的原始碼解讀方面的文章。

第二個原因是最近面試的過程中,發現很多候選人對redux的理解很淺,甚至有錯誤的理解。真正理解redux的思想的人非常好,更不要說理解它其中的精妙設計了。

因此就有了這篇文章的誕生。

REDUX是什麼

深入理解redux之前,首先來看下,redux是什麼,解決了什麼問題。

下面是redux官方給出的解釋:

Redux is a predictable state container for JavaScript apps.

上面的概念比較抽象,如果對redux不瞭解的人是很難理解的。

一個更容易被人理解的解釋(同樣是redux官方的解釋):

redux是flux架構的實現,受Elm啟發

首先科普兩個名字,flux和Elm。

flux

下面是facebook官方對flux的解釋:

Application Architecture for Building User Interfaces

更具體地說:

An application architecture for React utilizing a unidirectional data flow.

flux是隨著react一起推出的資料管理框架,它的核心思想是單項資料流。

一圖勝千言,讓我們通過圖來了解下flux

flux

通過react構建view,而react又是資料驅動的,那麼解決資料問題就解決了view的問題,通過flux架構管理資料,使得資料可預測。這樣 view也變得可預測。 非常棒~

Elm

Elm是一門編譯程式碼到javaScript的語言,它的特點是效能強和無執行時異常。Elm也有虛擬DOM的實現。

Elm的核心理念是使用Model構建應用,也就是說Model是應用的核心。 構建一個應用就是構建Model,構建更新Model的方式,以及如何構建Model到view的對映。

更多關於elm的介紹

瞭解了上面的東西,你會發現其實redux的任務就是管理資料。redux的資料流可以用下面的圖來標示:

redux

redux中核心就是一個單一的state。state通過閉包的形式存放在redux store中,保證其是隻讀的。如果你想要更改state,只能通過傳送action進行,action本質上就是一個普通的物件。

你的應用可以通過redux暴露的subscribe方法,訂閱state變化。如果你在react應用中使用redux,則表現為react訂閱store變化,並re-render檢視。

最後一個問題就是如何根據action來更新檢視,這部分是業務相關的。 redux通過reducer來更新state,關於reducer的介紹,我會在後面詳細介紹。

它精妙的設計我們在後面進行解讀。

最小化實現REDUX

其實寫一個redux並不困難。redux原始碼也就區區200行左右。 裡面大量使用高階函式,閉包,函式組合等知識。讓程式碼看起來更加簡短,結構更加清晰。

我們來寫一個"redux"

實現

我們要實現的redux主要有如下幾個功能:

  • 獲取應用state
  • 傳送action
  • 監聽state變化

讓我們來看下redux store暴漏的api

const store = {
  state: {}, // 全域性唯一的state,內部變數,通過getState()獲取
  listeners: [], // listeners,用來諸如檢視更新的操作
  dispatch: () => {}, // 分發action
  subscribe: () => {}, // 用來訂閱state變化
  getState: () => {}, // 獲取state
}
複製程式碼

我們來實現createStore,它返回store物件, store的物件結構上面已經寫好了。createStore是用來初始化redux store的,是redux最重要的api。 我們來實現一下:

createStore

const createStore = (reducer, initialState) => {
  // internal variables
  const store = {};
  store.state = initialState;
  store.listeners = [];
  
  // api-subscribe
  store.subscribe = (listener) => {
    store.listeners.push(listener);
  };
  // api-dispatch
  store.dispatch = (action) => {
    store.state = reducer(store.state, action);
    store.listeners.forEach(listener => listener());
  };
  
  // api-getState
  store.getState = () => store.state;
  
  return store;
};

複製程式碼

通過上面的20行左右的程式碼已經實現了redux的最基本功能了,是不是很驚訝?我們下面來試下。

使用

我們現在可以像使用redux一樣使用了我們的"redux"了。

以下例子摘自官網

你可以把下面這段指令碼加上我們上面實現的"redux",拷貝到控制檯執行,看下效果。是否和redux官方給的結果一致。

// reducer
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

let store = createStore(counter)

store.subscribe(() =>
  console.log(store.getState())
)


store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
複製程式碼

可以看出我們已經完成了redux的最基本的功能了。 如果需要更新view,就根據我們暴漏的subscribe去更新就好了,這也就解釋了 redux並不是專門用於react的,以及為什麼要有react-redux這樣的庫存在。

為了方便各個階段的人員能夠看懂,我省略了applyMiddleware的實現,但是不要擔心,我會在下面redux核心思想章節進行解讀。

REDUX核心思想

redux的核心思想出了剛才提到的那些之外。 個人認為還有兩個東西需要特別注意。 一個是reducer, 另一個是middlewares

reducer 和 reduce

reducer可以說是redux的精髓所在。我們先來看下它。reducer被要求是一個純函式

  • 被要求很關鍵,因為reducer並不是定義在redux中的一個東西。而是使用者傳進來的一個方法。
  • 純函式也很關鍵,reducer應該是一個純函式,這樣state才可預測(這裡應證了我開頭提到的Redux is a predictable state container for JavaScript apps.)。

日常工作我們也會用到reduce函式,它是一個高階函式。reduce一直是計算機領域中一個非常重要的概念。

reducer和reduce名字非常像,這是巧合嗎?

我們先來看下reducer的函式簽名:

fucntion reducer(state, action) {
    const nextState = {};
    // xxx
    return nextState;
}
複製程式碼

再看下reduce的函式簽名

[].reduce((state, action) => {
    const nextState = {};
    // xxx
    return nextState;
}, initialState)
複製程式碼

可以看出兩個幾乎完全一樣。最主要區別在於reduce的需要一個陣列,然後累計變化。 reducer則沒有這樣的一個陣列。

更確切地說,reducer累計的時間上的變化,reduce是累計空間上的變化。

如何理解reducer是累計時間上的變化?

我們每次通過呼叫dispatch(action)的時候,都會呼叫reducer,然後將reducer的返回值去更新store.state。

每次dispatch的過程,其實就是在空間上push(action)的過程,類似這樣:

[action1, action2, action3].reduce((state, action) => {
    const nextState = {};
    // xxx
    return nextState;
}, initialState)

複製程式碼

因此說,reducer其實是時間上的累計,是基於時空的操作。

middlewares

關於middleware的概念我們不多介紹, 感興趣可以訪問這裡檢視更多資訊。

如下可以實現middleware的效果:

store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
複製程式碼

上述程式碼會在dispatch前後進行列印資訊,這樣一個最簡單的中介軟體就實現了。如果再加上compose就可以將多箇中介軟體順序執行。

比如我們還定義了另外幾個相似的中介軟體,我們需要將多箇中介軟體按照一定順序執行,如下是redux applyMiddleware原始碼:

// 用reduce實現compose,很巧妙。
function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

// applyMiddleware 的原始碼
function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => null;
    let chain = [];

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 將middlewares組成一個函式
    // 也就是說就從前到後依次執行middlewares
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

// 使用
let store = createStore(
  todoApp,
  // applyMiddleware() tells createStore() how to handle middleware
  applyMiddleware(logger, dispatchAndLog)
)
複製程式碼

上面就是redux關於middleware的原始碼,非常簡潔。但是想要完全讀懂還是要花費點心思的。

首先redux通過createStore生成了一個原始的store(沒有被enhance),然後最後將原始store的dispatch改寫了,在呼叫原生的reducer之間,插入中介軟體邏輯(中介軟體鏈會順序依次執行). 程式碼如下:

function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    // let dispatch = xxxxx;
    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

然後我們將使用者傳入的middlewares順序執行,這裡藉助了compose,compose是函數語言程式設計中非常重要的一個概念,他的作用就是將多個函式組合成一個函式,compose(f, g, h)()最終生成的大概是這樣:

function(...args) {
  f(g(h(...args)))
}
複製程式碼

因此chain大概長這個樣子:

chain = [
  function middleware1(next) {
    // 內部可以通過閉包訪問到getState和dispath

  },
  function middleware2(next) {
    // 內部可以通過閉包訪問到getState和dispath

  },
  ...
]
複製程式碼

有了上面compose的概念之後,我們會發現每一個middleware的input 都是一個引數next為的function,第一個中介軟體訪問到的next其實就是原生store的dispatch。程式碼為證: dispatch = compose(...chain)(store.dispatch)。從第二個中介軟體開始,next其實就是上一個中介軟體返回的 action => retureValue 。 有沒有發現這個函式簽名就是dispatch的函式簽名。

output是一個引數為action的function, 返回的function簽名為 action => retureValue 用來作為下一個middleware的next。這樣middleware就可以選擇性地呼叫下一個 middleware(next)。

社群有非常多的redux middleware,最為經典的dan本人寫的redux thunk,核心程式碼只有兩行, 第一次看真的震驚了。從這裡也可以看出redux 的厲害之處。 基於redux的優秀設計,社群中出現了很多非常優秀的第三方redux中間價,比如redux-dev-tool, redux-log, redux-promise 等等,有機會我會專門出一個redux thunk的解析。

總結

本篇文章主要講解了redux是什麼,它主要做了什麼。然後通過不到20行程式碼實現了一個最小化的redux。最後深入講解了redux的核心設計reducer和middlewares。

redux還有一些非常經典的學習資源,這裡推薦redux作者本人的getting started with reduxYou Might Not Need Redux。學習它對於你理解redux以及如何使用redux管理應用狀態是非常有幫助的。

相關文章