我的源碼閱讀之路:redux源碼剖析

前言

用過react的小夥伴對redux其實並不陌生,基本大多數的React應用用到它。一般大家用redux的時候基本都不會單獨去使用它,而是配合react-redux一起去使用。剛學習redux的時候很容易弄混淆redux和react-redux,以爲他倆是同一個東西。其實不然,redux是javascript應用程序的可預測狀態容器,而react-redux則是用來連接這個狀態容器與react組件。可能前端新人對這兩者還是覺得很抽象,打個比方說,在一個普通家庭中,媽媽在家裏都是至高無上的地位,掌握家中經濟大權,家裏的經濟流水都要經過你的媽媽,而你的爸爸則負責從外面賺錢然後交給你的媽媽。這裏把你的媽媽類比成redux,而你的爸爸可以類比成react-redux,而外面的大千世界則是react組件。相信這樣的類比,大家對這react和react-redux的有了一個初步認識。本篇文章介紹的主要內容是對redux的源碼的分析,react-redux的源碼分析將會在我的下一篇文章中,敬請期待!各位小夥們如果覺得寫的不錯的話,麻煩多多點贊收藏關注哦!

redux的使用

在講redux的源碼之前,我們先回顧一下redux是如何使用的,然後我們再對照着redux的使用去閱讀源碼,這樣大家的印象可能會更加深刻點。先貼上一段demo代碼:

const initialState={
  cash:200,

}
const reducer=(state=initialState,action)=>{
  const {type,payload} = action;
  switch(type){
    case 'INCREMENT':
      return Object.assign({},state,{
        cash:state.cash+payload
      });
    case 'DECREMENT':
      return Object.assign({},state,{
        cash:state.cash-payload
      });
    default :
      return state;
  }
}

const reducers=Redux.combineReducers({treasury:reducer});

//創建小金庫
const store=Redux.createStore(reducers);

//當小金庫的現金髮生變化時,打印當前的金額
store.subscribe(()=>{
  console.log(`餘額:${store.getState().treasury.cash}`);
});

//小明爸爸發了工資300塊上交
store.dispatch({
  type:'INCREMENT',
  payload:300
});
//小明拿着水電費單交100塊水電費
store.dispatch({
  type:'DECREMENT',
  payload:100
});

上面這段代碼是一個非常典型的redux的使用,跟大家平時在項目裏用的不太一樣,可能有些小夥伴們不能理解,其實react-redux只不過在這種使用方法上做了一層封裝。等當我們弄清楚redux的使用,再去看react-redux源碼便會明白了我們在項目裏爲何是那種寫法而不是這種寫法。

說到redux的使用,不免要說一下action、reducer和store三者的關係。記得當初第一次使用redux的時候,一直分不清這三者的關係,感覺這三個很抽象很玄學,相信不少小夥伴們跟我一樣遇到過同樣的情況。其實並不難,我還是用文章開頭打的比方還解釋這三者的關係。

現在保險箱(store)裏存放200塊大洋。到月底了,小明的爸爸的單位發了工資總計300塊大洋,拿到工資之後第一件的事情就是上交,毫無疑問的,除非小明爸爸不要命了。小明的爸爸可以直接將這300塊大洋放到家裏的保險箱裏面嗎?顯然是不可以的,所以小明的爸爸得向小明的爸爸提交申請,而這個申請也就是我們所說的action。這個申請(action)包括操作類型和對應的東西,申請類型就是存錢(INCREMENT),對應的東西就是300塊大洋(payload)。此時小明的媽媽拿到這個申請之後,將根據這個申請執行對應的操作,這裏就是往保險箱裏的現金裏放300塊大洋進去,此時小明的媽媽乾的事情就是reducer乾的事情。當300塊大洋放完之後,小明的媽媽就通知家裏的所有人現在的小金庫的金額已經發生了變化,現在的餘額是500塊。當小明的爸爸收到這個通知之後,心的一塊大石頭也就放下來了。過了一會,小明回來了,並且拿着一張價值100塊的水電費的催收單。於是,小明想小明媽媽申請交水電費,小明媽媽從保險庫中取出來100塊給了小明,並通知了家裏所有人小金庫的金額又發生了變化,現在餘額400塊。

通過上面的例子,相信小夥們對三者的關係有了一個比較清晰的認識。現在我們已經理清楚了action、reducer和store三者的關係,並且也知道了redux是如何使用的了,現在將開始我們得源碼閱讀之旅。

redux項目結構

本篇文章是基於redux的4.0.0版本做的源碼分析,小夥伴們在對照源碼的時候,千萬別弄錯了。整個redux項目的源碼的閱讀我們只需要關注src的目錄即可。

圖片描述

這裏主要分爲兩大塊,一塊爲自定義的工具庫,另一塊則是redux的邏輯代碼。先從哪塊開始閱讀呢?我個人建議先閱讀自定義的工具庫這塊。主要有這麼兩個原因:第一個,這塊代碼比較簡單,容易理解,大家更能進入閱讀的狀態;第二個,redux邏輯代碼會用到這些自定義工具,先搞懂這些,對後續邏輯代碼的閱讀做了一個很好的鋪墊。下面我們正式開始我們的源碼閱讀之旅。

utils

actionTypes.js

const ActionTypes = {
  INIT:
    '@@redux/INIT' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.'),
  REPLACE:
    '@@redux/REPLACE' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.')
}

export default ActionTypes

這段代碼很好理解,就是對外暴露兩個action類型,沒什麼難點。但是我這裏想介紹的是Number.prototype.toString方法,估計應該有不少小夥伴們不知道toString是可以傳參的,toString接收一個參數radix,代表數字的基數,也就是我們所說的2進制、10進制、16進制等等。radix的取值範圍也很容易得出來,最小進制就是我們得二進制,所以redix>=2。0-9(10個數字)+a-z(26個英文字母)總共36個,所以redix<=36。總結一下2<=radix<=36,默認是10。基於這個特性我們可以寫一個獲取指定長度的隨機字符串的長度:

//獲取指定長度的隨機字符串
function randomString(length){
  let str='';
  while(length>0){
    const fragment= Math.random().toString(36).substring(2);
    if(length>fragment.length){
      str+=fragment;
      length-=fragment.length;
    }else{
      str+=fragment.substring(0,length);
      length=0;
    }
  }
  return str;
}

isPlainObject.js

export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

isPlainObject.js也很簡單,僅僅只是向外暴露了一個用於判斷是否簡單對象的函數。什麼簡單對象?應該有一些小夥伴不理解,所謂的簡單對象就是該對象的__proto__等於Object.prototype,用一句通俗易懂的話就是:

凡不是new Object()或者字面量的方式構建出來的對象都不是簡單對象

下面看一個例子:

class Fruit{
  sayName(){
    console.log(this.name)
  }
}

class Apple extends Fruit{
  constructor(){
    super();
    this.name="蘋果"
  }
}

const apple = new Apple();
const fruit = new Fruit();
const cherry = new Object({
  name:'櫻桃'
});
const banana = {
  name:'香蕉'
};

console.log(isPlainObject(apple));//false
console.log(isPlainObject(fruit));//false
console.log(isPlainObject(cherry));//true
console.log(isPlainObject(banana));//true

這裏可能會有人不理解isPlainObject(fruit)===false,如果對這個不能理解的話,自己後面要補習一下原型鏈的相關知識,這裏fruit.__proto__.__proto__纔等價於Object.prototype。

warning.js

export default function warning(message) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  try {
    throw new Error(message)
  } catch (e) {} 
}

這個也很簡單,僅僅是打印一下錯誤信息。不過這裏它的console居然加了一層判斷,我查閱了一下發現console其實是有兼容性問題,ie8及其以下都是不支持console的。哎,不僅感嘆一句!

如果說馬賽克阻礙了人類文明的進程,那ie便是阻礙了前端技術的發展。

邏輯代碼

到這裏我已經完成對utils下的js分析,很簡單,並沒有大家想象的那麼難。僅僅從這幾個簡單的js中,就牽引出好幾個我們平時不太關注的知識點。如果我們不讀這些源碼,這些容易被忽視的知識點就很難被撿起來,這也是爲什麼很多大佬建議閱讀源碼的原因。我個人認爲,閱讀源碼,理解原理是次要的。學習大佬的代碼風格、一些解決思路以及對自己知識盲點的點亮更爲重要。廢話不多說,開始我們下一個部分的代碼閱讀,下面的部分就是整個redux的核心部分。

index.js

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    "You are currently using minified code outside of NODE_ENV === 'production'. " +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
      'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

index.js是整個redux的入口文件,尾部的export出來的方法是不是都很熟悉,每個方法對應了一個js,這也是後面我們要分析的。這個有兩個點需要講一下:

第一個,__DO_NOT_USE__ActionTypes。 這個很陌生,平時在項目裏面我們是不太會用到的,redux的官方文檔也沒有提到這個,如果你不看源碼你可能就不知道這個東西的存在。這個幹嘛的呢?我們一點一點往上找,找到這麼一行代碼:

import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

這個引入的js不就是我們之前分析的utils的其中一員嗎?裏面定義了redux自帶的action的類型,從這個變量的命名來看,這是幫助開發者檢查不要使用redux自帶的action的類型,以防出現錯誤。

第二個,函數isCrushed。 這裏面定義了一個函數isCrushed,但是函數體裏面並沒有東西。第一次看的時候很奇怪,爲啥要這麼幹?相信有不少小夥伴們跟我有一樣的疑問,繼續往下看,緊跟着後面有一段代碼:

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    "You are currently using minified code outside of NODE_ENV === 'production'. " +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
      'to ensure you have the correct code for your production build.'
  )
}

看到process.env.NODE_ENV,這裏就要跟我們打包時用的環境變量聯繫起來。當process.env.NODE_ENV==='production'這句話直接不成立,所以warning也就不會執行;當process.env.NODE_ENV!=='production',比如是我們的開發環境,我們不壓縮代碼的時候typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed'也不會成立;當process.env.NODE_ENV!=='production',同樣是我們的開發環境,我們進行了代碼壓縮,此時isCrushed.name === 'string' && isCrushed.name !== 'isCrushed'就成立了,可能有人不理解isCrushed函數不是在的嗎?爲啥這句話就不成立了呢?其實很好理解,瞭解過代碼壓縮的原理的人都知道,函數isCrushed的函數名將會被一個字母所替代,這裏我們舉個例子,我將redux項目的在development環境下進行了一次壓縮打包。代碼做了這麼一層轉換:

未壓縮

function isCrushed() {}
if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
)

壓縮後

function d(){}"string"==typeof d.name&&"isCrushed"!==d.name

此時判斷條件就成立了,錯誤信息就會打印出來。這個主要作用就是防止開發者在開發環境下對代碼進行壓縮。開發環境下壓縮代碼,不僅讓我們

createStore.js

函數createStore接受三個參數(reducer、preloadedState、enhancer),reducer和enhancer我們用的比較多,preloadedState用的比較少。第一個reducer很好理解,這裏就不過多解釋了,第二個preloadedState,它代表着初始狀態,我們平時在項目裏也很少用到它,主要說一下enhancer,中文名叫增強器,顧名思義就是來增強redux的,它的類型的是Function,createStore.js裏有這麼一行代碼:

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

    return enhancer(createStore)(reducer, preloadedState)
  }

這行代碼展示了enhancer的調用過程,根據這個調用過程我們可以推導出enhancer的函數體的架子應該是這樣子的:

 function enhancer(createStore) {
    return (reducer,preloadedState) => {
         //邏輯代碼
        .......
    }
 }

常見的enhancer就是redux-thunk以及redux-saga,一般都會配合applyMiddleware一起使用,而applyMiddleware的作用就是將這些enhancer格式化成符合redux要求的enhancer。具體applyMiddleware實現,下面我們將會講到。我們先看redux-thunk的使用的例子:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

看完上面的代碼,可能會有人有這麼一個疑問“createStore函數第二個參數不是preloadedState嗎?這樣不會報錯嗎?” 首先肯定不會報錯,畢竟官方給的例子,不然寫個錯誤的例子也太大跌眼鏡了吧!redux肯定是做了這麼一層轉換,我在createStore.js找到了這麼一行代碼:

 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

當第二個參數preloadedState的類型是Function的時候,並且第三個參數enhancer未定義的時候,此時preloadedState將會被賦值給enhancer,preloadedState會替代enhancer變成undefined的。有了這麼一層轉換之後,我們就可以大膽地第二個參數傳enhancer了。

說完createStore的參數,下面我說一下函數createStore執行完之後返回的對象都有什麼?在createStore.js最下面一行有這一行代碼:

return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

他返回了有這麼幾個方法,其中前三個最爲常用,後面兩個在項目基本上不怎麼用,接下來我們去一一剖析。

定義的一些變量

let currentState = preloadedState //從函數createStore第二個參數preloadedState獲得
let currentReducer = reducer  //從函數createStore第一個參數reducer獲得
let currentListeners = [] //當前訂閱者列表
let nextListeners = currentListeners //新的訂閱者列表
let isDispatching = false

其中變量isDispatching,作爲鎖來用,我們redux是一個統一管理狀態容器,它要保證數據的一致性,所以同一個時間裏,只能做一次數據修改,如果兩個action同時觸發reducer對同一數據的修改,那麼將會帶來巨大的災難。所以變量isDispatching就是爲了防止這一點而存在的。

dispatch

function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

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

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

函數dispatch在函數體一開始就進行了三次條件判斷,分別是以下三個:

  • 判斷action是否爲簡單對象
  • 判斷action.type是否存在
  • 判斷當前是否有執行其他的reducer操作

當前三個預置條件判斷都成立時,纔會執行後續操作,否則拋出異常。在執行reducer的操作的時候用到了try-finally,可能大家平時try-catch用的比較多,這個用到的還是比較少。執行前isDispatching設置爲true,阻止後續的action進來觸發reducer操作,得到的state值賦值給currentState,完成之後再finally裏將isDispatching再改爲false,允許後續的action進來觸發reducer操作。接着一一通知訂閱者做數據更新,不傳入任何參數。最後返回當前的action。

getState

function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

getState相比較dispatch要簡單許多,返回currentState即可,而這個currentState在每次dispatch得時候都會得到響應的更新。同樣是爲了保證數據的一致性,當在reducer操作的時候,是不可以讀取當前的state值的。說到這裏,我想到之前一次的面試經歷:

面試官:執行createStore函數生成的store,可不可以直接修改它的state?

我:可以。(普羅大衆的第一反應)

面試官:你知道redux怎麼做到不能修改store的state嗎?

我:額......(處於懵逼狀態)

面試官:很簡單啊!重寫store的set方法啊!

那會沒看過redux的源碼,就被他忽悠了!讀完redux源碼之後,靠!這傢伙就是個騙子!自己沒讀過源碼還跟我聊源碼,無語了!當然,我自己也有原因,學藝不精,被忽悠了。我們這裏看了源碼之後,getState函數返回state的時候,並沒有對currentState做一層拷貝再給我們,所以是可以直接修改的。只是這麼修改的話,就不會通知訂閱者做數據更新。得出的結論是:

store通過getState得出的state是可以直接被更改的,但是redux不允許這麼做,因爲這樣不會通知訂閱者更新數據。

subscribe

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

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    let isSubscribed = true //表示該訂閱者在訂閱狀態中,true-訂閱中,false-取消訂閱

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false

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

在註冊訂閱者之前,做了兩個條件判斷:

  • 判斷監聽者是否爲函數
  • 是否有reducer正在進行數據修改(保證數據的一致性)

接下來執行了函數ensureCanMutateNextListeners,下面我們看一下ensureCanMutateNextListeners函數的具體實現邏輯:

 function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

邏輯很簡單,判斷nextListeners和currentListeners是否爲同一個引用,還記得dispatch函數中有這麼一句代碼以及定義變量時一行代碼嗎?

// Function dispatch
const listeners = (currentListeners = nextListeners)
// 定義變量
let currentListeners = []
let nextListeners = currentListeners

這兩處將nextListeners和currentListeners引用了同一個數組,另外定義變量時也有這麼一句話代碼。而ensureCanMutateNextListeners就是用來判斷這種情況的,當nextListeners和currentListeners爲同一個引用時,則做一層淺拷貝,這裏用的就是Array.prototype.slice方法,該方法會返回一個新的數組,這樣就可以達到淺拷貝的效果。

函數ensureCanMutateNextListeners作爲處理之後,將新的訂閱者加入nextListeners中,並且返回取消訂閱的函數unsubscribe。函數unsubscribe執行時,也會執行兩個條件判斷:

  • 是否已經取消訂閱(已取消的不必執行)
  • 是否有reducer正在進行數據修改(保證數據的一致性)

通過條件判斷之後,講該訂閱者從nextListeners中刪除。看到這裏可能有小夥伴們對currentListeners和nextListeners有這麼一個疑問?函數dispatch裏面將二者合併成一個引用,爲啥這裏有啥給他倆分開?直接用currentListeners不可以嗎?這裏這樣做其實也是爲了數據的一致性,因爲有這麼一種的情況存在。當redux在通知所有訂閱者的時候,此時又有一個新的訂閱者加進來了。如果只用currentListeners的話,當新的訂閱者插進來的時候,就會打亂原有的順序,從而引發一些嚴重的問題。

replaceReducer

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

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

這個函數是用來替換reducer的,平時項目裏基本很難用到,replaceReducer函數執行前會做一個條件判斷:

  • 判斷所傳reducer是否爲函數

通過條件判斷之後,將nextReducer賦值給currentReducer,以達到替換reducer效果,並觸發state更新操作。

observable

  /**
   * Interoperability point for observable/reactive libraries.
   * @returns {observable} A minimal observable of state changes.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */

這裏沒貼代碼,因爲這塊代碼我們不需要掌握。這個observable函數,並沒有調用,即便暴露出來我們也辦法使用。所以我們就跳過這塊,如果有興趣的話,可以去作者給的github的地址瞭解一下。


講完這幾個方法之後,還有一個小細節需要說一下,createStore函數體裏有這樣一行代碼。

dispatch({ type: ActionTypes.INIT })

爲啥要有這麼一行代碼?原因很簡單,假設我們沒有這樣代碼,此時currentState就是undefined的,也就我說我們沒有默認值了,當我們dispatch一個action的時候,就無法在currentState基礎上做更新。所以需要拿到所有reducer默認的state,這樣後續的dispatch一個action的時候,纔可以更新我們的state。

combineReducers.js

這個js對應着redux裏的combineReducers方法,主要作用就是合併多個reducer。現在我們先給一個空的函數,然後再一步步地根據還原源碼,這樣大家可能理解得更爲透徹點。

//reducers  Object類型  每個屬性對應的值都要是function
export default function combineReducers(reducers) {
    ....
}

第一步:淺拷貝reducers

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)
}

這裏定義了一個finalReducers和finalReducerKeys,分別用來拷貝reducers和其屬性。先用Object.keys方法拿到reducers所有的屬性,然後進行for循環,每一項可根據其屬性拿到對應的reducer,並淺拷貝到finalReducers中,但是前提條件是每個reducer的類型必須是Function,不然會直接跳過不拷貝。

第二步:檢測finalReducers裏的每個reducer是否都有默認返回值

function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don't want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      )
    }

    const type =
      '@@redux/PROBE_UNKNOWN_ACTION_' +
      Math.random()
        .toString(36)
        .substring(7)
        .split('')
        .join('.')
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don't try to handle ${
            ActionTypes.INIT
          } or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

export default function combineReducers(reducers) {
    //省略第一步的代碼
    ......
    let shapeAssertionError
    try {
        assertReducerShape(finalReducers)
    } catch (e) {
        shapeAssertionError = e
    }
}

assertReducerShape方法主要檢測兩點:

  • 不能佔用<redux/*>的命名空間
  • 如果遇到未知的action的類型,不需要要用默認返回值

如果傳入type爲 @@redux/INIT<隨機值> 的action,返回undefined,說明沒有對未
知的action的類型做響應,需要加默認值。如果對應type爲 @@redux/INIT<隨機值> 的action返回不爲undefined,但是卻對應type爲 @@redux/PROBE_UNKNOWN_ACTION_<隨機值> 返回爲undefined,說明佔用了 <redux/*> 命名空間。整個邏輯相對簡單,好好自己梳理一下。

第三步:返回一個函數,用於代理所有的reducer


export default function combineReducers(reducers) {
    //省略第一步和第二步的代碼
    ......
    let unexpectedKeyCache
        if (process.env.NODE_ENV !== 'production') {
        unexpectedKeyCache = {}
    }
    return function combination(state = {}, action) {
        if (shapeAssertionError) {
            throw shapeAssertionError
        }

        if (process.env.NODE_ENV !== 'production') {
            const warningMessage = getUnexpectedStateShapeWarningMessage(
                state,
                finalReducers,
                action,
                unexpectedKeyCache
            )
            if (warningMessage) {
                warning(warningMessage)
            }
        }

        let hasChanged = false
        const nextState = {}
        for (let i = 0; i < finalReducerKeys.length; i++) {
            const key = finalReducerKeys[i]
            const reducer = finalReducers[key]
            const previousStateForKey = state[key]
            const nextStateForKey = reducer(previousStateForKey, action)
            if (typeof nextStateForKey === 'undefined') {
            const errorMessage = getUndefinedStateErrorMessage(key, action)
                throw new Error(errorMessage)
            }
        nextState[key] = nextStateForKey
        hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        return hasChanged ? nextState : state
    }    
}

首先對傳入的state用getUnexpectedStateShapeWarningMessage做了一個異常檢測,找出state裏面沒有對應reducer的key,並提示開發者做調整。接着我們跳到getUnexpectedStateShapeWarningMessage裏,看其實現。

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'

  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (action && action.type === ActionTypes.REPLACE) return

  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}

getUnexpectedStateShapeWarningMessage接收四個參數 inputState(state)、reducers(finalReducers)、action(action)、unexpectedKeyCache(unexpectedKeyCache),這裏要說一下unexpectedKeyCache是上一次檢測inputState得到的其裏面沒有對應的reducer集合裏的異常key的集合。整個邏輯如下:

  1. 前置條件判斷,保證reducers集合不爲{}以及inputState爲簡單對象
  2. 找出inputState裏有的key但是 reducers集合裏沒有key
  3. 如果是替換reducer的action,跳過第四步,不打印異常信息
  4. 將所有異常的key打印出來

getUnexpectedStateShapeWarningMessage分析完之後,我們接着看後面的代碼。

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state

首先定義了一個hasChanged變量用來表示state是否發生變化,遍歷reducers集合,將每個reducer對應的原state傳入其中,得出其對應的新的state。緊接着後面對新的state做了一層未定義的校驗,函數getUndefinedStateErrorMessage的代碼如下:

function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || 'an action'

  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}

邏輯很簡單,僅僅做了一下錯誤信息的拼接。未定義校驗完了之後,會跟原state作對比,得出其是否發生變化。最後發生變化返回nextState,否則返回state。

compose.js

這個函數主要作用就是將多個函數連接起來,將一個函數的返回值作爲另一個函數的傳參進行計算,得出最終的返回值。以烹飪爲例,每到料理都是從最初的食材經過一道又一道的工序處理纔得到的。compose的用處就可以將這些烹飪工序連接到一起,你只需要提供食材,它會自動幫你經過一道又一道的工序處理,烹飪出這道料理。

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)))
}

上面是es6的代碼,可能小夥伴們並不是很好理解,爲了方便大家理解,我將其轉換成es5代碼去做講解。

function compose() {
  var _len = arguments.length;
  var funcs = [];
  for (var i = 0; i < _len; i++) {
    funcs[i] = arguments[i];
  }

  if (funcs.length === 0) {
    return function (arg) {
      return arg;
    };
  }

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

  return funcs.reduce(function (a, b) {
    return function () {
      return a(b.apply(undefined, arguments));
    };
  });
}

梳理一下整個流程,大致分爲這麼幾步:

  1. 新建一個新數組funcs,將arguments裏面的每一項一一拷貝到funcs中去
  2. 當funcs的長度爲0時,返回一個傳入什麼就返回什麼的函數
  3. 當funcs的長度爲1時,返回funcs第0項對應的函數
  4. 當funcs的長度大於1時,調用Array.prototype.reduce方法進行整合

這裏我們正好複習一下數組的reduce方法,函數reduce接受下面四個參數

  • total 初始值或者計算得出的返回值
  • current 當前元素
  • index 當前元素的下標
  • array 當前元素所在的數組

示例:

const array = [1,2,3,4,5,6,7,8,9,10];
const totalValue=array.reduce((total,current)=>{
  return total+current
}); //55

這裏的compose有個特點,他不是從左到右執行的,而是從右到左執行的,下面我們看個例子:

const value=compose(function(value){
  return value+1;
},function(value){
  return value*2;
},function(value){
  return value-3;
})(2);
console.log(value);//(2-3)*2+1=-1

如果想要其從左向右執行也很簡單,做一下順序的顛倒即可。

===> 轉換前 return a(b.apply(undefined, arguments));
===> 轉換後 return b(a.apply(undefined, arguments));

applyMiddleware.js

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

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

    return {
      ...store,
      dispatch
    }
  }
}

前面我們講enhancer的時候,提到過這個applyMiddleware,現在我們將二者的格式對比看一下。

// enhancer
 function enhancer(createStore) {
    return (reducer,preloadedState) => {
         //邏輯代碼
        .......
    }
 }
//applyMiddleware
function //applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        //邏輯代碼
        ....... 
    }
 }

通過二者的對比,我們發現函數applyMiddleware的返回就是一個enhancer,下面我們再看其具體實現邏輯:

  1. 通過createStore方法創建出一個store
  2. 定一個dispatch,如果在中間件構造過程中調用,拋出錯誤提示
  3. 定義middlewareAPI,有兩個方法,一個是getState,另一個是dispatch,將其作爲中間件調用的store的橋接
  4. middlewares調用Array.prototype.map進行改造,存放在chain
  5. 用compose整合chain數組,並賦值給dispatch
  6. 將新的dispatch替換原先的store.dispatch

看完整個過程可能小夥伴們還是一頭霧水,玄學的很!不過沒關係,我們以redux-thunk爲例,模擬一下整個過程中,先把redux-thunk的源碼貼出來:

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;

哈哈哈!看完redux-thunk的源碼之後是不是很奔潰,幾千star的項目居然就幾行代碼,頓時三觀就毀了有木有?其實源碼沒有大家想象的那麼複雜,不要一聽源碼就慌。穩住!我們能贏!根據redux-thunk的源碼,我們拿到的thunk應該是這樣子的:

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

我們經過applyMiddleware處理一下,到第四步的時候,chain數組應該是這樣子的:

const newDispatch;
const middlewareAPI={
  getState:store.getState,
  dispatch: (...args) => newDispatch(...args)
}
const { dispatch, getState } = middlewareAPI;
const  fun1 = (next)=>{
  return action => {
    if (typeof action === 'function') {
        return action(dispatch, getState);
    }
    return next(action);
  }
}
const chain = [fun1]

compose整合完chain數組之後得到的新的dispatch的應該是這樣子:

const newDispatch;
const middlewareAPI={
  getState:store.getState,
  dispatch: (...args) => newDispatch(...args)
}
const { dispatch, getState } = middlewareAPI;
const next = store.dispatch;
newDispatch = action =>{
  if (typeof action === 'function') {
    return action(dispatch, getState);
  }
  return next(action);
}

接下來我們可以結合redux-thunk的例子來模擬整個過程:

function makeASandwichWithSecretSauce(forPerson) {
  return function (dispatch) {
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    );
  };
}
// store.dispatch就等價於newDispatch
store.dispatch(makeASandwichWithSecretSauce('Me'))

====> 轉換
const forPerson = 'Me';
const action = (dispatch)=>{
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    );
}
newDispatch()

===> typeof action === 'function' 成立時

 ((dispatch)=>{
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    );
  })( (...args) => newDispatch(...args), getState)

====> 計算運行結果
const forPerson = 'Me';
const dispatch = (...args) => newDispatch(...args) ;
fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
// 其中:
function fetchSecretSauce() {
  return fetch('https://www.google.com/search?q=secret+sauce');
}
function makeASandwich(forPerson, secretSauce) {
  return {
    type: 'MAKE_SANDWICH',
    forPerson,
    secretSauce
  };
}

function apologize(fromPerson, toPerson, error) {
  return {
    type: 'APOLOGIZE',
    fromPerson,
    toPerson,
    error
  };
}
====> 我們這裏只計算Promise.resolve的結果,並且假設fetchSecretSauce返回值爲'666',即sauce='666'

const forPerson = 'Me';
const dispatch = (...args) => newDispatch(...args) ;
dispatch({
    type: 'MAKE_SANDWICH',
    'Me',
    '666'
})
====> 爲了方便對比,我們再次轉換一下

const action = {
    type: 'MAKE_SANDWICH',
    'Me',
    '666'
};

const next = store.dispatch

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

newDispatch(action)

====> 最終結果
store.dispatch({
    type: 'MAKE_SANDWICH',
    'Me',
    '666'
});

以上就是redux-thunk整個流程,第一次看肯能依舊會很懵,後面可以走一遍,推導一下加深自己的理解。

bindActionCreators.js

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreators針對於三種情況有三種返回值,下面我們根據每種情況的返回值去分析。(爲了方便理解,我們選擇在無集成中間件的情況)

typeof actionCreators === 'function'

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
const actionFun=bindActionCreator(actionCreators, dispatch)

===> 整合一下

const fun1 = actionCreators;
const dispatch= stror.dispatch;
const actionFun=function () {
    return dispatch(fun1.apply(this, arguments))
 }

根據上面的推導,當變量actionCreators的類型爲Function時,actionCreators必須返回一個action。

typeof actionCreators !== 'object' || actionCreators === null

 throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )

提示開發者actionCreators類型錯誤,應該是一個非空對象或者是函數。

默認

 const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators

通過和第一種情況對比發現,當actionCreators的每一項都執行一次第一種情況的操作。換句話說,默認情況是第一種情況的集合。


以上是對bindActionCreators的剖析,可能小夥伴們對這個還是不夠理解,不過沒有關係,只要知道bindActionCreators幹了啥就行。bindActionCreators是需要結合react-redux一起使用的,由於本篇文章沒有講解react-redux,所以這裏我們不對bindActionCreators做更深入的講解。下篇文章講react-redux,會再次提到bindActionCreators。

結語

到這裏整個redux的源碼我們已經剖析完了,整個redux代碼量不是很大,但是裏面的東西還是很多的,邏輯相對來說有點繞。不過沒關係,沒有什麼是看了好幾次都看不懂的,如果有那就再多看幾次嘛!另外再多一嘴,如果想快讀提高自己的小夥伴們,我個人是強烈推薦看源碼的。正所謂“近朱者赤,近墨者黑”,多看看大神的代碼,對自己的代碼書寫、代碼邏輯、知識點查缺補漏等等方面都是很大幫助的。就拿我自己來說,我每次閱讀完一篇源碼之後,都受益匪淺。可能第一次看源碼,有着諸多的不適應,畢竟萬事開頭難,如果強迫自己完成第一次的源碼閱讀,那往後的源碼閱讀將會越來越輕鬆,對自己的提升也就越來越快。各位騷年們,擼起袖子加油幹吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章