我的原始碼閱讀之路:redux原始碼剖析

ruichengping發表於2018-09-19

前言

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

相關文章