redux v4.0.0 原始碼分析

-A-發表於2019-04-01

文章來自我個人的Github

這周決定宅著不出去做完平日裡沒做完的事情。其中之一就是分析redux的原始碼。我看的版本是v4.0.0。Redux的作者是Dan Abramov。俄羅斯人。非常好的人,還回復過我的水文

Redux本身程式碼量是很少的。典型的文件比程式碼多系列。測試也寫得很全。Redux的標語是

Predictable state container for JavaScript apps

Predictable可以預測的狀態管理容器。可預測最主要的就是可以做time machine,時間旅行。可以回滾到上次的狀態。Redux對初學者來說可能是非常complex的。並且有些過於囉嗦。Redux本身不屬於React的一部分。React-Redux和Redux是兩個不同的專案。Redux本身只是一個很小的狀態管理庫。可以應用到其他的框架上。

看原始碼之前要做的事情就是摸熟這個框架或者codebase的用法。這樣就不會覺得陌生。

首先要做的事情是從Gihub把Redux的原始碼克隆下來。

git clone git@github.com:reactjs/redux.git

克隆下來後為了除錯程式碼。我自己寫了個webpack.config.js,開個dev server配合vscode 的debugger in chrome進行除錯。其實我也不知道正規的看程式碼方法吧(其實是找了很久沒找到,又不想除錯test case)。反正現在也不糾結,我就先這樣。貼一下配置。

先進入redux根目錄建立webpack.config.js。再開個debugger目錄。

touch webpack.config.js
mkdir debugger
複製程式碼

裝依賴

yarn add -D webpack
yarn add -D webpack-cli
yarn add -D html-webpack-plugin
yarn add -D webpack-dev-server
yarn
複製程式碼

額 不做這一步也可以,只是我喜歡這樣。然後弄完可以直接用vscode除錯了。

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './debugger/index.js',
  devtool: 'inline-source-map',
  output: {
    path: __dirname + '/dist',
    filename: 'index_debugger_bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './debugger/index.html'
    })
  ]
}
複製程式碼

debugger/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  哭哭惹
</body>
</html>
複製程式碼

debugger/index.js

import * as redux from '../src'

console.log(redux)
複製程式碼

package.json

"scripts": {
  "debugger": "webpack-dev-server --open --port=2333"
}
複製程式碼

改一下vscode除錯的配置就可以了。詳細的自己看一下吧。source-map必須要開不然這外掛就是shit。

專案結構

以下是每個檔案的用處。懶得打字所以一大部分都是抄襲的。貼上原文地址。因為原作者的版本比較舊了。所以自己補充了幾個文紅沒寫到的。

applyMiddlewar.js 使用自定義的 middleware 來擴充套件 Redux

bindActionCreators.js 把 action creators 轉成擁有同名 keys 的物件,使用時可以直接呼叫

combineReducers.js 一個比較大的應用,需要對 reducer 函式 進行拆分,拆分後的每一塊獨立負責管理 state 的一部分

compose.js 從右到左來組合多個函式,函式程式設計中常用到

createStore.js 建立一個 Redux Store 來放所有的state

utils/warnimng.js 控制檯輸出一個警告,我們可以不用看

utils/actionTypes.js redux內部使用的3個action

utils/isPlanObject.js 判斷物件是不是純物件

createStore

所有的一切都基於createStore建立出來的store。可以說是一切的開始。createStore函式接受三個引數。

function createStore(reducer, preloadedState, enhancer)
複製程式碼

createStore暴露出的方法有以下這些

方法名 作用
dispatch dispatch一個action觸發狀態變化,呼叫reducer建立新的store
subscribe 新增listener狀態改變的時候會被觸發listener的回撥函式
getState 讀取狀態樹管理的狀態
replaceReducer 替換當前使用的Reducer。如果你想要動態替換Reducer的話可以用到。
$$observable 私有屬性 不知道幹嘛的 叫我看這個

createStore函式就是一個Observser實現的話也沒啥好看的。讀一讀程式碼就能理解了。這裡就不詳細寫了。不是很懂的地方有一個就是ensureCanMutateNextListeners的作用。暫時我還沒有理解。

MiddleWare 實現

redux實現middleware和compose函式有很多的關係。這個compose函式非常的簡單。核心是這樣。我刪掉了一些不(特別)重要的程式碼。

const compose = (...funcs) => {
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼

這個compose只做一件事情。把函式組合起來。就是函數語言程式設計的那個組合。返回一個從右到左執行的compose function。比如傳入(a,b,c)這樣的函式引數這樣的執行順序: c(b(a(...args)))。我們可以做個實驗,你可以直接把程式碼複製到控制檯裡面執行。

const a = () => console.log('a')
const b = () => console.log('b')
const c = () => console.log('c')
compose(a,b,c)()
複製程式碼

輸出的結果會是: c,b,a。但是如果我們把這些函式做一個更加高階的處理。讓函式第一次執行的時候返回函式。變成這樣的話。會發生一個很有趣的現象。

const a = ( next ) => ( param ) => { console.log(param); next('b'); }
const b = ( next ) => ( param ) => { console.log(param); next('c'); }
const c = ( next ) => ( param ) => { console.log(param); next('d'); }
const d = (param) => console.log(param)
const cp = compose(a,b,c)(d)
// execute
cp('a')
複製程式碼

輸出的結果會是a,b,c,d。和之前的函式版本相比。此時的compose function函式擁有了控制是否執行下一個函式的能力。並且通過呼叫next來執行下一個。同時它變成正序的。Redux利用了這一特性。

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

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
    }
  }
}
複製程式碼

Redux把store => next => action的最外層通過執行middlewares.map(middleware => middleware(middlewareAPI))傳入引數把函式變成了next => action的形式完成了中介軟體的模式。

combineReducer 實現

combineReducer的作用是把零零散散的reducer拼起來或者說把複雜的單個reducer拆分成小的reducer。實現的原理都在程式碼上。以下程式碼做了一些精簡的處理,去掉了一些不是特別重要的警告。


export default function combineReducers(reducers) {
  // 獲取reducers的key
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 處理reducerKeys
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    // 非開發環境下 如果reducers[key]為undefined 丟擲警告
    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]
    }
  }
  // 獲取finalReducerKeys的所有key
  const finalReducerKeys = Object.keys(finalReducers)

  // 組合state 原理是每次執行呼叫每個傳過來的reducers
  // 最終通過nextState拼出一個最大的state
  // 通過`hasChanged`做了快取處理
  return function combination(state = {}, action) {
    // 快取處理
    let hasChanged = false
    const nextState = {}
    // 呼叫每個reducer ..
    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)
      // 如果nextStateForKey執行後的結果為undefined 說明該reducer返回的
      // 結果是undefined 會丟擲異常
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 和上次的狀態比較 如果一致就為false。返回上一次的狀態。
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}
複製程式碼

bindActionCreators 實現

bindActionCreators的最常見用的地方可能是在react-redux呼叫mapDispatchToProps的時候。

const TodoAction = (...args) => { type: 'TODO', { ...args } }

function mapDispatchToProps(dispatch) {
  actions: bindActionCreators(TodoAction, dispatch)
}
複製程式碼

不用bindActionCreators的話會是這樣寫。就如下面的bindActionCreator函式基本一致。要注意原始碼是沒有s那個。

呼叫有s那個傳一個物件。寫起來會是這樣。原理是遍歷執行bindActionCreators

import * as TodoActions from '../actions/todo'

function mapDispatchToProps(dispatch) {
  actions: bindActionCreators(TodoActions, dispatch)
}
複製程式碼

原始碼如下:

function mapDispatchToProps(dispatch) {
  actions:  (...args) => {
     dispatch(TodoActions(args))
  }
}
複製程式碼

原始碼如下:

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

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
}
複製程式碼

isDispatching的作用

這個變數用來處理reducer 中 呼叫 dispatch,導致死迴圈 的情況。

如下程式碼,在reducer裡dispatch 又被呼叫,這種情況redux是不允許的。

var reducer = function(state, action){
    switch (action.type) {
        case 'add_todo':
            store.dispatch({type: 'yyy'}); // 呼叫B
            return state;
        default:
            return state;
    }
};

var store = redux.createStore(reducer, []);
store.dispacth({type: 'xxx'}); // 呼叫A
複製程式碼

相關文章