【KT】輕鬆搞定Redux源碼解讀與編程藝術

前言

📢 博客首發 : 阿寬的博客javascript

在本文開始以前,嘮叨幾句話吧,那就是本文有點長,且有部分源碼等;前幾天有幸和寒雁老哥聊了一小會,他說我如今已經懂怎麼寫文章階段,建議下一個階段能穩下來,而後去寫一些有深度的東西,而不是浮在表面上;上週六去聽了同公司已出書的挖坑的張師傅的技術寫做分享。vue

因而我沉默了一下,聽了一些前輩的建議,我決定,奧力給,作一個技術深度專區的文章~,儘可能每個月一更,不過每更一次,篇幅都會較長,儘量的分享一個較爲完整的主題。因此打個預防針吧,但願各位小夥伴,能靜下心來看,你們一同進步~java

🔥 爲何這個專欄叫【KT】,我這人比較 low,專欄中文叫: 阿寬技術深文,K 取自阿寬中的寬,T,Technology,技術,有逼格。能夠,感受本身吹牛逼的技術又進了一步。react

本文流水線

因爲時間關係,而且在組裏引出了 react 中狀態管理的論戰,圍繞着 hox、mobx、redux 進行一波交流,因此第四步的動手實踐,我會晚點再更,接下來這段時間打算研究一下 hoxmobx 的一個內部實現原理,而後動手實踐寫下 demo,在組裏評審一波,取其精華去其糟粕,說不定又是一個新的產物?想一想就很激動有意思呢~git

別噴,造輪子只是爲了學習~程序員

本文適合人員

  • 🍉 吃瓜羣衆
  • redux 入門級選手
  • 想了解 redux 內幕
  • 想知道 redux 的編程藝術

看完這篇文章你能學到什麼

  • 科普下所涉及的 函數式編程洋蔥模型相關知識
  • 性感小彭,手把手帶你看 redux 源碼
  • 瞭解 redux 庫中的一些設計理念
  • 不再怕面試官問你 redux 爲何返回一個新的 state
  • (我保證下篇確定是動手實踐!!!)

正文開始

背景介紹

博主在 18 年末面試的時候,面試官看我簡歷,問: 「我看你簡歷,vue 和 react 都用過,你能說一下Vue 和 React 的區別嘛?」,當時逼逼賴賴說了一下,也不知道說的對不對,而後在說到 vuex 和 redux 的時候,血案發生了,面試官問了一句,爲何 Redux 老是要返回一個新的 state ?返回舊的 state 爲何不行 ?面試結果不用說,畢竟當時我也不是這麼瞭解嘛~github

當時面試完了以後,抽空把 redux 的源碼看了一遍,ojbk,確實看的比較暈,記得當時看的時候,redux 還沒引入 TS,前段時間,想深刻去了解一下 redux,誰知,一發不可收拾,鬼知道我在看的過程說了多少句 WC,牛逼...面試

雖然這篇文章,是針對 redux 入門選手寫的,但因爲我這該死的儀式感,說個東西以前,仍是得簡單介紹一下~vuex

redux 是啥?

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

✋ Redux is a predictable state container for JavaScript apps.

咩呀?聽不懂啊?稍等稍等,在作解釋以前,請容許我問你個問題,react 是單向數據流仍是雙向數據流?,若是你回答的是雙向數據流,ok,拜拜 👋,出門左轉,若是你回答的是單向數據流,嗯,咱們仍是好兄弟~

要理解 redux 是啥子,先看我畫的一個圖 👇

咱們知道哈,react 中,有 props 和 state,當咱們想從父組件給子組件傳遞數據的時候,可經過 props 進行數據傳遞,若是咱們想在組件內部自行管理狀態,那能夠選擇使用 state。可是呢,咱們忽略了 react 的自身感覺~

react 它是單向數據流的形式,它不存在數據向上回溯的技能,你要麼就是向下分發,要麼就是本身內部管理。(咋地,挑戰權威呢?你覺得能夠如下犯上嗎?)

小彭一聽,「 哎不對啊,不是能夠經過回調進行修改父組件的 state 嗎?」 是的,確實能夠。先說說咱們爲啥使用 redux,通常來說,咱們在項目中能用到 redux 的,幾乎都算一個完整的應用吧。這時候呢,若是你想兩個兄弟組件之間進行交流,互相八卦,交換數據,你咋整?

咱們模擬一個場景,Peng 組件和 Kuan 組件想共享互相交換一些數據,按照 react 單向數據流的方式,該怎麼解決?

這個圖應該都看得懂哈,也就是說,咱們兄弟組件想互相交流,交換對方的數據,那麼惟一的解決方案就是:提高 state,將本來 Peng、Kuan 組件的 state 提高到共有的父組件中管理,而後由父組件向下傳遞數據。子組件進行處理,而後回調函數回傳修改 state,這樣的 state 必定程度上是響應式的。

這會存在什麼問題?你會發現若是你想共享數據,你得把全部須要共享的 state 集中放到全部組件頂層,而後分發給全部組件。

爲此,須要一個庫,來做爲更加牛逼、專業的頂層 state 發給各組件,因而,咱們引入了 redux,這就是 redux 的簡單理解。

這就是咱們總能看到,爲啥在根App組件都有這麼個玩意了。

function App() {
  return (
    <Provider store={store}> ... </Provider>
  );
}
複製代碼

三大原則

阿寬這裏就默認你們都會使用 redux 了,不會使用的你就去啃啃文檔,寫個 demo 你就會了嘛,不過呢,仍是要說一說 redux 的三大原則的~

  • 單一數據源 : 整個應用的 state 都存儲在一顆 state tree 中,而且只存在於惟一一個 store 中
  • state 是隻讀的 : 惟一改變 state 的方法只能經過觸發 action,而後經過 action 的 type 進而分發 dispatch 。不能直接改變應用的狀態
  • 狀態修改均由純函數完成 : 爲了描述 action 如何改變 state tree,須要編寫 reducers

基礎知識儲備

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(
  reducerList,
  (initialState = {}),
  applyMiddleware(thunkMiddleware)
)
複製代碼

redux 中最核心的 API 就是: createStore, 經過 createStore 方法建立的 store 是一個對象,它自己包含 4 個方法 :

  • getState() : 獲取 store 中當前的狀態。
  • subscribe(listener) : 註冊一個監聽者,它在 store 發生變化時被調用。
  • dispatch(action) : 分發一個 action,並返回這個 action,這是惟一能改變 store 中數據的方式。
  • replaceReducer(nextReducer) : 更新當前 store 裏的 reducer,通常只會在開發模式中調用該方法。

Aciton

Action 是把數據從應用傳到 store 的有效載荷。它是 store 數據的惟一來源。簡單來講,Action 就是一種消息類型,他告訴 Redux 是時候該作什麼了,並帶着相應的數據傳到 Redux 內部。

Action 就是一個簡單的對象,其中必需要有一個 type 屬性,用來標誌動做類型(reducer 以此判斷要執行的邏輯),其餘屬性用戶能夠自定義。如:

const KUAN_NEED_GRID_FRIEND = 'KUAN_NEED_GRID_FRIEND'
複製代碼
// 一個action對象
// 好比此action是告訴redux,阿寬想要一個女友
{
  type: KUAN_NEED_GRID_FRIEND,
  params: {
    job: '程序員',
    username: '阿寬'
  }
}
複製代碼

咱們來了解一個知識點: Action Creator,看看官網中的介紹 : Redux 中的 Action Creator 只是簡單的返回一個 Action,咱們通常都會這麼寫~

function fetchWishGridFriend(params, callback) {
  return {
    type: KUAN_NEED_GRID_FRIEND,
    params,
    callback,
  }
}
複製代碼

咱們知道哈,Redux 由 Flux 演變而來,在傳統的 Flux 中, Action Creators 被調用以後常常會觸發一個 dispatch。好比是這樣的 👇

// 傳統 Flux
function fetchFluxAction(params, callback) {
  const action = {
    type: KUAN_NEED_GRID_FRIEND,
    params,
    callback,
  }
  dispatch(action)
}
複製代碼

可是在 redux 中,由於 store(上邊說過了)中存在 dispatch 方法的,因此咱們只須要將 Action Creators 返回的結果傳給 dispatch() ,就完成了發起一個 dispatch 的過程,甚至於建立一個被綁定的 Action Creators 來自動 dispatch ~

// 普通dispatch
store.dispatch(fetchWishGridFriend(params, () => {}))

// 綁定dispatch
const bindActionCreatorsDemo = (params, callback) => (store.dispatch) =>
  store.dispatch(fetchWishGridFriend(params, callback))
bindActionCreatorsDemo() // 就能實現一個dispatch action
複製代碼

👉 在你的代碼中,必定能夠找獲得bindActionCreators() 這玩意,由於通常狀況下,咱們都會使用 react-redux 提供的 connect() 幫助器,bindActionCreators() 能夠自動把多個 action 建立函數綁定到 dispatch() 方法上。

Reducers

Reducers 必須是一個純函數,它根據 action 處理 state 的更新,若是沒有更新或遇到未知 action,則返回舊 state;不然返回一個新 state 對象。注意:不能修改舊 state,必須先拷貝一份 state,再進行修改,也可使用 Object.assign 函數生成新的 state,具體爲何,咱們讀源碼的時候就知道啦~

舉個例子 🌰

// 用戶reducer
const initialUserState = {
    userId: undefined
}

function userReducer = (state = initialUserState, action) {
  switch(action.type) {
    case KUAN_NEED_GRID_FRIEND:
      return Object.assign({}, state, {
        userId: action.payload.data
      })
    default:
      return state;
  }
}
複製代碼

在看源碼以前,我舉個形象生動的 🌰 ,幫助你們理解理解。

小彭想請個假去旅遊,按照原流程,必須得由從 小彭申請請假 -> 部門經理經過 -> 技術總監經過 -> HR 經過(單向流程),小彭的假條不能直接到 HR 那邊。看下圖 👇

阿寬看到小彭請假旅遊,也想請一波,因而想 copy 一份小彭的請假事由(兄弟組件進行數據共享)那咋辦,他不能直接從小彭那拿數據,因此他只能傻乎乎的經過部門經理、技術總監,一路「闖關」到 HR 那,指着 HR 說,你把小彭的請假表給我複印一份,我也要請假。

小彭和阿寬想進行數據之間共享,只能經過共有的 boss(HR)

當咱們用了 redux 以後呢,就變成這屌樣了 👇 看懂扣 1,看不懂釦眼珠子

入手源碼

淦!!! 又到了我最討厭的源碼解讀了,由於講源碼太難了,不是源碼難,而是怎麼去講比較難,畢竟我自己理解的和認識的 redux,不必定是正確的,同時我也不想直接貼一大堆代碼上去,你不就是不想看源碼纔看的這篇文章嗎

不過沒辦法,理解萬歲。幸虧 redux 的源碼文件相對較少,你們一塊兒奧力給!

🎉 直接看源碼,github 戳這裏,咱們能夠看到這樣的文件架構

├── utils
│   ├── actionTypes
│   ├── isPlainObject
│   ├── warning
│   └─
│
├── applyMiddleware
├── bindActionCreatorts
├── combineReducers
├── compose
├── createStore
├── index.js
│
└─
複製代碼

很少吧?說多的出門左轉不送。看源碼要從 index.js 開始入手,跟着鏡頭,咱們去看看這個文件有啥玩意。其實沒啥重要玩意, 就是把文件引入而後 export

// index.js
import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
...

export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose }
複製代碼

咱們先來看第一行代碼,import createStore from './createStore',😯,這個我知道,這不就是 redux 中最核心的 API 之一嗎?讓咱們去揭開它的面紗~

createStore 至上

// API
const store = createStore(reducers, preloadedState, enhance)
複製代碼

初次看,不知道這三個參數啥意思?不慌,先抽根菸,打開百度翻譯,你就知道了。(由於源碼中有對這三個參數給出解釋)

/** * 建立一個包含狀態樹的Redux存儲 * 更改store中數據的惟一方法是在其上調用 `dispatch()` * * 你的app中應該只有一個store,指定狀態樹的不一樣部分如何響應操做 * 你可使用 `combineReducers` 將幾個reducer組合成一個reducer函數 * * @param {Function} reducer 給定當前狀態樹和要處理的操做的函數,返回下一個狀態樹 * * @param {any} [preloadedState] 初始狀態. 你能夠選擇將其指定爲中的universal apps服務器狀態,或者還原之前序列化的用戶會話。 * 若是你使用 `combineReducers` 來產生 root reducer 函數,那麼它必須是一個與 `combineReducers` 鍵形狀相同的對象 * * @param {Function} [enhancer] store enhancer. 你能夠選擇指定它來加強store的第三方功能 * 好比 middleware、time travel、persistence, Redux附帶的惟一商店加強器是 `applyMiddleware()` * * @returns {Store} Redux Store,容許您讀取狀態,調度操做和訂閱更改。 */
複製代碼

瞭解這三個參數的意思以後呢,咱們再看看它的返回值,中間作了啥先不用管。上邊有說過,調用 createStore 方法建立的 store 是一個對象,它包含 4 個方法,因此代碼確定是這樣的,不是我剁 diao !

// createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false // 是否正在分發事件

  function getState() {
    // ...
    return currentState
  }

  function subscribe(listener) {
    // ...
  }

  function dispatch(action) {
    // ...
    return action
  }

  function replaceReducer(nextReducer) {
    // ...
  }

  function observable() {
    // ...
  }

  dispatch({ type: ActionTypes.INIT })

  // ...
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable,
  }
}
複製代碼

沙箱設計

就這些代碼,想必都看得懂,可是不得不佩服寫這段代碼的人啊!!首先經過閉包進行了內部變量私有化,外部是沒法訪問閉包內的變量。其次呢經過對外暴露了接口,以達到外部對內部屬性的訪問。

這不就是沙箱嗎?沙箱,就是讓你的程序跑在一個隔離的環境下,不對外界的其餘程序形成影響。咱們的 createStore 對內保護內部數據的安全性,對外經過開發的接口,進行訪問和操做。🐂 🍺 ~

subscribe/dispatch

💥 建議直接去看源碼文件,由於裏邊對於每個接口的註釋很詳細~

不難看到,上邊經過 subscribe進行接口註冊訂閱函數,咱們能夠細看這個函數作了什麼事情~

function subscribe(listener) {
  ...

  let isSubscribed = true

  ensureCanMutateNextListeners();
  nextListeners.push(listener)

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

    // reducer執行中,你可能沒法取消store偵聽器
    if (isDispatching) {}

    isSubscribed = false

    // 從 nextListeners 中去除掉當前 listener
    ensureCanMutateNextListeners()
    const index = nextListeners.indexOf(listener)
    nextListeners.splice(index, 1)
}
複製代碼

其實這個方法主要作的事情就是 : 註冊 listener,同時返回一個取消事件註冊的方法。當調用 store.dispatch 的時候調用 listener ~

思路真的是很嚴謹了,定義了 isSubscribedisDispatching來避免意外的發生,同時還對傳入對 lister 進行類型判斷。考慮到有些人會取消訂閱,因此還提供了一個取消訂閱的unsubscribe

緊接着咱們再來看看 dispatch,主要是用與發佈一個 action 對象,前邊有說到了,你想要修改 store 中的數據,惟一方式就是經過 dispatch action,咱們來看看它作了什麼事情~

function dispatch(action) {
  if (!isPlainObject(action)) {
  }

  if (typeof action.type === 'undefined') {
  }

  // 調用dispatch的時候只能一個個調用,經過dispatch判斷調用的狀態
  if (isDispatching) {
  }

  try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
  }

  // 遍歷調用各個listener
  const listeners = (currentListeners = nextListeners)
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i]
    listener()
  }
  return action
}
複製代碼

不是吧,阿 sir,這麼嚴格,前邊就作了各類限制,下邊這段 try {} finally {} 也是神操做啊,爲了保證 isDispatch在函數內部狀態的一致,在 finally 的時候都會將其改成 false。牛掰~

從源碼註釋裏邊,我也看到這麼一段話 ~

It will be called any time an action is dispatched, and some part of the state tree may potentially have changed.

You may then call getState() to read the current state tree inside the callback.

意味着,當你執行了以前訂閱的函數 listener 以後,你必須,經過 store.getState() 去那最新的數據。由於這個訂閱函數 listener 是沒有參數的,真的很嚴格。

bindActionCreators

老舍先生的《四世同堂》十九中有一句化 : 「他以爲老大實在有可愛的地方,因而,他決定趁熱打鐵,把話都說淨。」,是的,趁熱打鐵,既然咱們說到了 dispatch(action), 那咱們接着說一說: bindActionCreators

不知道各位有沒有寫過這樣的代碼~

import { bindActionCreators } from 'redux';
import * as pengActions from '@store/actions/peng';
import * as kuanActions from '@store/actions/kuan';
import * as userActions from '@store/actions/user';

const mapDispatchToProps => dispatch => {
  return {
    ...bindActionCreators(pengActions, dispatch);
    ...bindActionCreators(kuanActions, dispatch);
    ...bindActionCreators(userActions, dispatch);
  }
}
複製代碼

咱們來講說,這個 bindActionCreators 它到底作了什麼事情。首先來看官方源碼註釋:

  • 將值爲 action creators 的對象轉換爲具備相同鍵的對象
  • 將每一個函數包裝爲「dispatch」調用,以即可以直接調用它們
  • 固然你也能夠調用 store.dispatch(MyActionCreator.doSomething)
function bindActionCreator(actionCreator, dispatch) {
  return function (this, ...args) {
    return dispatch(actionCreator.apply(this, args))
  }
}

// bindActionCreators 指望獲得的是一個 Object 做爲 actionCreators 傳進來
export default function bindActionCreators(actionCreators, dispatch) {
  // 若是隻是傳入一個action,則經過bindActionCreator返回被綁定到dispatch的函數
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
  }

  const boundActionCreators = {} // 最終導出的就是這個對象
  for (const key in actionCreator) {
    const actionCreator = actionCreator[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
複製代碼

對了,這裏你們必定要記住,Action 的取名儘可能不要重複,舉個 🌰

小彭和阿寬都有一個需求,那就是發起一個修改年齡的 action,原本兩不相干,井水不犯河水,因而他兩洋洋灑灑的在代碼中寫下了這段代碼 ~

// pengAction.js
export function changeAge(params, callback) {
  return {
    type: 'CHANGE_AGE',
    params,
    callback,
  }
}

// kuanAction.js
export function changeAge(params, callback) {
  return {
    type: 'CHANGE_AGE',
    params,
    callback,
  }
}
複製代碼

你說巧不巧,產品讓阿華去作一個需求,須要點擊按鈕的時候,把小彭和阿寬的年齡都改了。阿華想用 bindActionCreators 裝 B,因而寫下了這段代碼

const mapDispatchToProps => dispatch => {
  return {
    ...bindActionCreators(pengActions, dispatch);
    ...bindActionCreators(kuanActions, dispatch);
  }
}
複製代碼

按照咱們對 bindActionCreators 的源碼理解,它應該是這樣的 😯

pengActions = {
  changeAge: action,
}

export default function bindActionCreators(pengActions, dispatch) {
  // ...
  const boundActionCreators = {}

  for (const key in pengActions) {
    // key就是changeAge
    const actionCreator = pengActions[changeAge]
    // ...
    boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
  }
  return boundActionCreators
}
複製代碼

因此最終,這段代碼結果是這樣的

const mapDispatchToProps => dispatch => {
  return {
    changeAge, // ...bindActionCreators(pengActions, dispatch);
    changeAge // ...bindActionCreators(kuanActions, dispatch);
  }
}
複製代碼

問題知道在哪了吧,因此如何解決呢,我我的見解, 你要麼就 actionName 不要同樣,能夠叫 changePengAgechangeKuanAge,要麼就是多包一個對象。

const mapDispatchToProps => dispatch => {
  return {
    peng: {
      ...bindActionCreators(pengActions, dispatch);
    },
    kuan: {
      ...bindActionCreators(kuanActions, dispatch);
    }
  }
}
複製代碼

combineReducers

既然前邊都說了,整個應用的 state 都存儲在一顆 state tree 中,而且只存在於惟一一個 store 中, 那麼咱們來看看這到底是何方神聖~

小彭項目初次搭建的時候,要求小,狀態管理比較方便,因此呢,都放在了一個 reducer 中,後邊隨着不斷迭代,因而不斷的往這個 reducer 中塞數據。

典型的屁股決定腦殼,因而有一天,可能某個天使,給 redux 的開發團隊提了一個 issue, 「哎呀,你能不能提供一個 API,把個人全部 reducer 都整合在一塊啊,我想分模塊化的管理狀態」

好比用戶模塊,就叫 userReducer,商品模塊,咱們叫 shopReducer,訂單模塊,咱們稱之爲 orderReducer。既然那麼多個 reducer,該如何合併成一個呢 ?

因而 redux 提供了 combineReducers 這個 API,看來 redux 的時間管理學學的很好,你看,這麼多個 reducer ,都能整合在一塊兒,想必花了很大的功夫~

那咱們看看 combineReducers 作了什麼事情吧 ~ 在此以前,咱們看看咱們都怎麼用這玩意的~

// 兩個reducer
const pengReducer = (state = initPengState, action) => {}
const kuanReducer = (state = initKuanState, action) => {}

const appReducer = combineReducers({
  pengReducer,
  kuanReducer,
})
複製代碼
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers) // 獲得全部的reducer名

  // 1. 過濾reducers中不是function的鍵值對,過濾後符合的reducer放在finalReducers中
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // 2. 再一次過濾,判斷reducer中傳入的值是否合法
  let shapeAssertionError: Error
  try {
    // assertReducerShape 函數用於遍歷finalReducers中的reducer,檢查傳入reducer的state是否合法
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // 3. 返回一個函數
  return function combination(state, action) {
    // 嚴格redux又上線了,各類嚴格的檢查
    // ...

    let hasChanged = false // 就是這逼,用來標誌這個state是否有更新
    const nextState = {}

    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      // 這也就是爲何說combineReducers黑魔法--要求傳入的Object參數中,reducer function的名稱和要和state同名的緣由
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]

      // 將reducer返回的值,存入nextState
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey

      // 若是任一state有更新則hasChanged爲true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
複製代碼

這個源碼其實很少也不難,跟着阿寬這樣看下來,也不是很吃力吧?那這裏就延伸了一個問題,爲何 redux 必須返回一個新的 state ? 返回舊的不行嗎 ?

返回一個新的 state

伊索寓言有句話我特喜歡 : 逃出陷阱比掉入陷阱難之又難,是的,reducer 也有陷阱~ 衆所周知啊,reducer 必須是個純函數,這裏有小夥伴懵逼了,這 TM 怎麼又多出了一個知識點,不用管,我也不打算多講。自行百度~

咱們來看看,通常狀況下咱們都怎麼寫 reducer 的

function pengReducer(state = initialState, action) {
  switch (action.type) {
    // 這種方式
    case 'CHANGE_AGE':
      return {
        ...state,
        age: action.data.age,
      }
    // 或者這種方式都行
    case 'ADD_AGE':
      return Object.assign({}, state, {
        age: action.data.age,
      })
  }
}
複製代碼

假設,咱們不是這麼寫的,咱們直接修改 state,而不是返回一個新的 state,會是怎樣的結果~

function pengReducer(state = initialState, action) {
  switch (action.type) {
    // 或者這種方式都行
    case 'CHANGE_AGE':
      state.age = action.data.age
      return state
  }
}
複製代碼

當咱們觸發 action 以後,你會發出 : 臥槽,頁面爲何沒變化 ...

回到咱們的源碼,咱們能夠來看~

const nextStateForKey = reducer(previousStateForKey, action)
複製代碼

這裏主要就是,獲得經過 reducer 執行以後的 state,它不是一個 key,它是一個 state,而後呢,往下繼續執行了這行代碼~

hasChanged = hasChanged || nextStateForKey !== previousStateForKey
複製代碼

比較新舊兩個對象是否一致,進行的是淺比較法,因此,當咱們 reducer 直接返回舊的 state 對象時,Redux 認爲沒有任何改變,從而致使頁面沒有更新。

❓ 這就是爲何!返回舊的 state 不行,須要返回一個新的 state 緣由。咱們都知道啊,在 JS 中,比較兩個對象是否徹底同樣,那隻能深比較,然而,深比較在真實的應用中代碼是很是大的,很是耗性能的,而且若是你的對象嵌套足夠神,那麼須要比較的次數特別多~

因此 redux 就採起了一個較爲「委婉」的解決方案:當不管發生任何變化時,都要返回一個新的對象,沒有變化時,返回舊的對象~

applyMiddleware

跪了,感受 redux 源碼中,最難的莫過於中間件了,在說這玩意以前,咱們先來聊聊,一些有趣的東西~

一提到 react,不知道你們第一印象是什麼,可是有一個詞,我以爲絕大部分對人都應該聽過,那就是 : 💗 函數式編程 ~

函數式編程

  1. 函數是第一等公民

怎麼理解,在 JS 中,函數能夠看成是變量傳入,也能夠賦值給一個變量,甚至於,函數執行的返回結果也能夠是函數。

const func = function () {}

// 1. 看成參數
function demo1(func) {}

// 2. 賦值給另外一個變量
const copy_func = func

// 3. 函數執行的返回結果是函數
function demo2() {
  return func
}
複製代碼
  1. 數據是不可變的(Immutable)

在函數式編程語言中,數據是不可變的,全部的數據一旦產生,就不能改變其中的值,若是要改變,那就只能生成一個新的數據。

可能有些小夥伴會有過這個庫 : seamless-immutable ,在 redux 中,強調了,不能直接修改 state 的值(上邊有說了,不聽課的,出去吃屁),只能返回一個新的 state ~

  1. 函數只接受一個參數

怎麼理解,大夥估計都寫了好久的多參數,看到這個懵了啊,我也懵了,可是這就是規矩,無規矩,不成方圓 ~

因此當你看中間件的代碼時,你就不會奇怪了,好比這行代碼 ~

const middleware = (store) => (next) => (action) => {}
複製代碼

換成咱們可以理解的形式,那就是 :

const middleware = (store) => {
  return (next) => {
    return (action) => {}
  }
}
複製代碼

這裏有人就疑問了,尼瑪,這不就是依賴了三個參數嗎,那能不能這樣寫啊?

const middleware = (store, next, action) => {}
複製代碼

💐 just you happy ! 你高興就好,可是函數式編程就是要求,只能有一個參數,這是規矩,懂 ? 在我地盤,你就只能給我裝慫 !

組合 compose

說說組合 compose,這個是個啥玩意,咱們來看一段代碼 :

const compose = (f, g) => {
  return (x) => {
    return f(g(x))
  }
}

const add = function (x) {
  return x + 2
}

const del = function (x) {
  return x - 1
}

// 使用組合函數,🧬 基因突變,強強聯合
const composeFunction = compose(add, del)(100)
複製代碼

猜一下,執行 composeFunction 打印什麼?答對的,給本身鼓個掌 👏

好了,我已經把最爲強大的忍術: 函數式編程術語之 compose 組合函數,教給你了~

洋蔥模型

這裏又有小夥伴懵圈了,怎麼又來了一個知識點?不慌,容阿寬給你簡單介紹一下 ? 咱們上邊說了 compose 函數,那麼組合函數和洋蔥模型有什麼關係呢 ?

洋蔥模型是本質上是一層層的處理邏輯,而在函數式編程世界裏,意味着用函數來作處理單元。先不說其餘,咱們先上一個 🌰,幫助你們理解~

let middleware = []
middleware.push((next) => {
  console.log('A')
  next()
  console.log('A1')
})
middleware.push((next) => {
  console.log('B')
  next()
  console.log('B1')
})
middleware.push((next) => {
  console.log('C')
})

let func = compose(middleware)
func()
複製代碼

猜猜打印順序是個啥 ?沒錯,打印結果爲 : A -> B -> C -> B1 -> A1

哎喲,不錯哦,好像有點感受了。當程序運行到 next() 的時候會暫停當前程序,進入下一個中間件,處理完以後纔會仔回過頭來繼續處理。

這兩張圖應該是老圖了,並且是嘮嗑到洋蔥模式必貼的圖,就跟你喝酒同樣,必定要配花生米(別問爲何,問就是規矩)

咱們看這張圖,頗有意思哈,會有兩次進入同一個中間件的行爲,並且是在全部第一次的中間件執行以後,才依次返回上一個中間件。你品,你細品~

源碼解讀

好了,不逼逼了,因爲個人查克拉不足,關於其餘的函數式編程的忍術要求,就不一一講了,ok,這裏打了個預防針,咱們再來看看 applyMiddleware 到底作了什麼 喪心病狂 的事情吧~

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, ...args) => {
    const store = createStore(reducer, ...args)
    let dispatch: Dispatch = () => {}

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args),
    }
    const chain = middlewares.map((middleware) => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch,
    }
  }
}
複製代碼

代碼極其簡短,讓咱們看一下,幹了啥事~ 首先呢返回一個以 createStore 爲參數的匿名函數,而後呢,這個函數返回另外一個以 reducer, ...args (實際就是 initState, enhancer) 爲參數的匿名函數, 接着定義了一個鏈 chain,這個就頗有意思了。

const chain = middlewares.map((middleware) => middleware(middlewareAPI))
複製代碼

咱們先是把傳入的 middlewares 進行剝皮,並給中間件 middleware 都以咱們定義的 middlewareAPI 做爲參數注入,因此咱們每個中間件的上下文是 dispatch 和 getState,爲何?爲何要注入這兩個玩意?

  • getState:這樣每一層洋蔥均可以獲取到當前的狀態。

  • dispatch:爲了能夠將操做傳遞給下一個洋蔥

ok,這樣執行完了以後,chain 實際上是一個 (next) => (action) => { ... } 函數的數組,也就是中間件剝開後返回的函數組成的數組。以後咱們以 store.dispatch 做爲參數進行注入~ 經過 compose中間件數組內剝出來的高階函數進行組合造成一個調用鏈。調用一次,中間件內的全部函數都將被執行。

// 或許換成這種形式,你更加能明白~
function compose(...chain) {
  return store.dispatch => {
    // ...
  }
}
複製代碼

redux 中的 compose

上邊說到,這逼就是將咱們傳入的 chain 造成一個調用鏈,那咱們 see see,它是怎麼作到的~

export default 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)))
}
複製代碼

還記得上邊教大家的組合 compose 嗎,咱們試着還原一下常人能看得懂的樣子 ~

(a, b) => (...args) => a(b(...args))

// 常人能看得懂的
(a, b) => {
  return (...args) {
    return a(b(...args))
  }
}
複製代碼

兩個字,牛皮 🐂🍺 不得不感慨,果真是大佬。 那麼下邊,咱們來一步步捋一捋這究竟是個啥東西。

  • 拋出第一個問題?快速搶答,dispatch 是用來幹嗎的?

🙋 我會我會,dispatch 是用來分發 action 的,good,那麼,咱們能夠獲得第一個函數

(store.dispatch) => (action) => {}
複製代碼

問題又來了,咱們的 compose 通過一頓騷操做後獲得的一組結構相同的函數,最終合併成一個函數。

  • 這裏拋出第二個問題,既要傳遞 dispatch,又要傳遞 action,那麼咱們怎麼搞?高階函數用起來
middleware = (store.dispatch, store.getState) => (next) => (action) => {}
複製代碼

ok,那有人就好奇了,這個 next 是個啥玩意啊?其實傳入中間件的 next 實際上就是 store.dispatch,奇奇怪怪的問題又出現了

  • 拋出問題三,咱們怎樣讓每個中間件持有最終的 dispatch

redux 開發者利用了閉包的特性,將內部的 dispatch 與外部進行強綁定,MD,🐂🍺

// 實例demo
let dispatch = () => {}

middlewares.map((middleware) =>
  middleware({
    getState,
    dispatch() {
      return dispatch
    },
  })
)
複製代碼

因此你應該可以明白源碼中這段代碼的真諦了吧?

//真實源碼
let middlewareAPI = {
  getState: store.getState,
  dispatch: (action, ...args) => dispatch(action, ...args),
}

// 其實你把 middlewareAPI 寫到 middleware 裏邊,就等價於上邊那玩意了
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
複製代碼

而後接下來咱們須要作些什麼?重要的話說三遍,上邊說了兩邊,這邊再說一邊,compose 處理後獲得的是一個函數,那麼這個函數到底該怎樣調用呢。傳入 store.dispatch 就行了呀~

// 真實源碼
dispatch = compose(...chain)(store.dispatch)
複製代碼

這段代碼實際上就等價於:

dispatch = chain1(chain2(chain3(store.dispatch)))
複製代碼

chain一、chain二、chain3 就是 chain 中的元素,進行了一次柯里化,穩。dispatch 在這裏邊扮演了什麼角色?

  • 綁定了各個中間件的 next,說了 next 實際上就是 store.dispatch
  • 暴露一個接口用來接受 action

你能夠這麼理解,中間件其實就是咱們自定義了一個 dispatch,而後這個 dispatch 會按照洋蔥模型進行 pipe

what the fuck ! 🐂 🍺 爆粗口就對了。不過這裏我仍是有一個疑惑,但願看到這的大哥們,能解疑一下 ~

留給個人疑惑: 爲何在 middlewareAPI 中,dispatch 不是直接寫成 store.dispatch, 而是用的匿名函數的閉包引用?

// 爲何不這麼寫....
let middlewareAPI = {
  getState: store.getState,
  dispatch: (action) => store.dispatch(action),
}
複製代碼

結尾

到了這一步,還沒聽懂的小夥伴,能夠再多看一遍,正所謂溫故而知新,多看看,多捋捋,就能知道啦~

這篇文章寫了我五天,設計到的知識點略多,能夠說是有些知識點,現學現用,不過問題不大,由於延伸的知識點不是本文的重點~經過寫這篇文章,能夠說是加深了我對 redux 的認識。不知道有沒有小夥伴跟我同樣,想去看源碼,正面剛,剛不過,去看一些博客文章對其解讀,又太難,可能我沒 get 到做者想表達的意思,或者是對於其中的一些知識點,一帶而過,因此我想把我遇到的問題,在學習的路上踩到的坑,跟你們一同分享,固然個人理解也不必定正確,理解有誤可一同交流。奧力給,不說了,我去準備動手作 demo 了,期待下一篇吧 ~

相關連接

相關文章
相關標籤/搜索