redux 源碼分析

背景

在以前的文章redux從入門到實踐當中對redux的使用進行了說明,此次就來看下它的源碼,從而進一步的熟悉它。node

構建

相關git地址react

git clone https://github.com/reduxjs/redux.git
複製代碼

構建文檔是CONTRBUTING.mdwebpack

package.json

"main": "lib/redux.js",
// ...
"scripts": {
    "clean": "rimraf lib dist es coverage",
    "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "lint": "eslint src test",
    "pretest": "npm run build",
    "test": "jest",
    "test:watch": "npm test -- --watch",
    "test:cov": "npm test -- --coverage",
    "build": "rollup -c",
    "prepare": "npm run clean && npm run format:check && npm run lint && npm test",
    "examples:lint": "eslint examples",
    "examples:test": "cross-env CI=true babel-node examples/testAll.js"
  }
複製代碼

package.json當中能夠看到redux的入口文件是lib/redux.js,這個文件是經過打包出來的。那咱們看下打包配置文件rollup.configgit

{
    input: 'src/index.js',
    output: { file: 'lib/redux.js', format: 'cjs', indent: false },
    external: [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {})
    ],
    plugins: [babel()]
  },
  // ...省略
複製代碼

能夠看到入口文件應該是src/index.jsgithub

咱們來看下src/index.jsweb

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'

/* * This is a dummy function to check if the function name has been altered by minification. * If the function has been minified and NODE_ENV !== 'production', warn the user. */
// 是否壓縮代碼,若是運行環境在非生成環境可是代碼被壓縮了,警告用戶
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 setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
      'to ensure you have the correct code for your production build.'
  )
}

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

複製代碼

src/index.js主要是將方法暴露出來,給使用者使用npm

  • createStore 用於建立store
  • combineReducers 用於組合成rootReducers,由於在外部初始化store時,只能傳入一個reducers
  • bindActionCreators 組裝了dispatch方法
  • applyMiddleware 合併多箇中間件
  • compose 將中間件(middleware)和加強器(enhancer)合併傳入到createStore

combineReducers

src/combineReducers.jsjson

export default function combineReducers(reducers) {
  // 遍歷出reducers的對象名稱
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 遍歷reducers名稱
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      // 若是reducer對應的值是 undefined 輸出警告日誌
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    // 當這個reducer是函數 則加入到finalReducers對象中
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 讀取出過濾後的reducers
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  // 開發環境將unexpectedKeyCache設置爲空對象
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    // 檢查各個reducers是否考慮過defualt的狀況,不能返回undefined
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // combineRducers返回的是一個方法,dispatch最後執行的方法
  return function combination(state = {}, action) {
    // 若是reducer檢查出有問題就會拋出異常
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    // 開發者環境下
    if (process.env.NODE_ENV !== 'production') {
      // 對過濾後的reducers和初始化的state進行檢查
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      // 若是有問題就會輸出
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {}
    // 遍歷過濾後的reducers
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      // 根據key取出對應reducer
      const reducer = finalReducers[key]
      // 根據key將state對應的值取出
      const previousStateForKey = state[key]
      // 執行咱們reducer的方法,nextStateForKey就是根據actionType返回的state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 檢查nextStateForKey是不是undefined
      if (typeof nextStateForKey === 'undefined') {
        // 若是undefined就報錯
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 將合併後的state賦值到nextState當中
      nextState[key] = nextStateForKey
      // 若是state的值改變過 則hasChanged置爲true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 若是改變過 則返回新的state否則則是原有的state({})
    // 若是state不傳值就是 空的對象{}
    return hasChanged ? nextState : state
  }
}

複製代碼

combineReducers方法會先對傳入的reducers進行校驗,reducer的類型只能是function,最後返回的是個方法,這個方法很關鍵,由於在disptach時,最後執行的就是這個方法。這個方法有2個參數stateaction,方法內會根據傳入的action返回state,最後會比較新舊的state,若是不相等,則會返回新的state,若是相等會返回新的state。redux

那麼若是咱們直接對store的state進行操做而不是經過dispatch會發生呢,好比說咱們這樣api

const state = store.getState();
state.name = 'baifann';
複製代碼

咱們看一下combineReducers中的getUnexpectedStateShapeWarningMessage這個方法,它會檢查store中初始化的state的key有沒有在各個子reducer當中,若是沒有就會報錯。

/** * @inputState 初始化的state * @reducers 已通過過濾的reducers * @action 隨着combinRecuers傳入的action * @unexpectedKeyCache 開發者環境是一個空的對象,生成環境是undefined */
/** * 檢查合法的reducers是否存在 * * */
function getUnexpectedStateShapeWarningMessage( inputState, reducers, action, unexpectedKeyCache ) {
  // 將過濾的reducers的名取出
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    // 若是這個action的type是預製的ActionTypes.INIT
    // argumentName就是preloadedState argument passed to createStore
    // 否則是previous state received by the reducer
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'
  // 若是過濾後的reducer長度爲0
  // 則返回字符串告知沒有一個合法的reducer(reducer必須是function類型)
  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.'
    )
  }
  // 判斷輸入的state是不是obj對象
  // 若是不是 則返回字符串告知inputState不合法
  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('", "')}"`
    )
  }

  // 傳入的state進行遍歷
  // 若是state的對象名不包含在reducer中 而且不包含在unexpectedKeyCache對象中
  // unexpectedKeyCache在開發者環境是一個空的對象 所以只要state的對象名不包含在reducer中,這個key就會
  // 保存到 unexpectedKeys 當中
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  // 將inputState的key所有設置爲true
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  // 若是這個action的type是定義的定義中的ActionTypes.REPLACE 就返回不執行
  if (action && action.type === ActionTypes.REPLACE) return
  // 若是unexpectedKeys中有值,則發出警告
  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.`
    )
  }
}
複製代碼

compose

compose會返回一個方法,這個方法能夠將傳入的方法依次執行

export default function compose(...funcs) {
  // 若是函數方法爲0 則
  if (funcs.length === 0) {
    // 會將參數直接返回
    return arg => arg
  }

  // 若是隻傳入一個方法則會返回這個方法
  if (funcs.length === 1) {
    return funcs[0]
  }
  // a爲上一次回調函數返回的值 b爲當前值
  // 效果就是不斷執行數組中的方法最後返回時一個函數
  // 這個方法能夠將全部數組中的方法執行
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

複製代碼

createStore

咱們接下來看下createStore.js這個文件,它只暴露出了createStore的方法,在createStore中,初始化了一些參數,同時返回了一個store,store中包括了dispatchsubscribegetStatereplaceReducer,[$$observable]: observable

import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'


export default function createStore(reducer, preloadedState, enhancer) {
  // 若是初始化的state是一個方法而且enhancer也是方法就會報錯
  // 若是enhancer是方法若是第4個參數是方法就會報錯
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function'
    )
  }

  // 若是初始化的state是方法,enhancer的參數爲undefined
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // enhancer賦值初始話的stae
    enhancer = preloadedState
    // preloadedState賦值爲undefined
    preloadedState = undefined
    // 這裏是一個兼容2個參數的處理,當參數僅爲2個 第二個參數爲enhcaner時的處理
  }

  // 若是enhancer 不是undefined
  if (typeof enhancer !== 'undefined') {
    // 若是enhancer不是方法會報錯
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // 返回enhancer的方法
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 若是reducer不是方法 則報錯
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }


  // rootReducer賦值到currentReducer當中 實際是一個函數
  let currentReducer = reducer
  // 當前store中的state 默認是初始化的state
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      // 淺拷貝一個數組 雖然是淺拷貝 可是currentListener不會被nextListener改變
      nextListeners = currentListeners.slice()
    }
  }

  function getState() {
      // 省略代碼...
  }

  function subscribe(listener) {
      // 省略代碼...
  }

  function dispatch(action) {
      // 省略代碼...
  }

  function replaceReducer(nextReducer) {
      // 省略代碼...
  }

  function observable() {
      // 省略代碼...
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  // 執行dispatch 來初始化store中的state
  dispatch({ type: ActionTypes.INIT })

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

複製代碼

看完以後,咱們可能在這個地方有一點疑惑,就是這裏

// 若是enhancer 不是undefined
  if (typeof enhancer !== 'undefined') {
    // 若是enhancer不是方法會報錯
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // 返回enhancer的方法
    return enhancer(createStore)(reducer, preloadedState)
  }
複製代碼

這個返回的是什麼呢,咱們知道applyMiddleware返回的其實就是enhancer,那咱們結合在一塊兒看一下

applyMiddleware

import compose from './compose'

 /** * 建立一個store的加強器,使用中間件來包裝dispath方法,這對於各類任務來講都很方便 * 好比以簡潔的方式進行異步操做,或記錄每一個操做有效負載 * * 查看`redux-thunk`包,這是一箇中間件的例子 * * 由於中間件多是異步的,因此應該是對個enhancer傳參 * * 每個中間件都要提供dispatch和getstate兩個方法做參數 * */
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 建立一個store ...args爲reducer, preloadedState
    const store = createStore(...args)
    // 默認定義disptach方法,是一個拋出的報錯
    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作了包裝,會在dispatch的同時同時將中間件的方法也返回
    dispatch = compose(...chain)(store.dispatch)
    // 返回store中的屬性以及新的dispatch方法
    return {
      ...store,
      dispatch
    }
  }
}

複製代碼

若是直接返回了enhancer那麼返回的其實也是store,可是這個store中的dispatch被包裝過,當dispatch被執行時,會將全部中間件也依次執行。

接下來分析一下createStore中的方法

  • getState
  • subscribe
  • dispatch
  • replaceReducer
  • observable

getState

很簡單,就是返回currentState

/** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */
  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.'
      )
    }
    // 返回state
    return currentState
  }
複製代碼

subscribe

這是將一個回調加入到監聽數組當中,同時,它會返回一個註銷監聽的方法。

function subscribe(listener) {
    // 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
  
    ensureCanMutateNextListeners()
    // 把listenr加入到nextListeners的數組當中
    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
      // 這裏作了個拷貝 作的全部操做不影響currentListener
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 在nextListener將它去除
      nextListeners.splice(index, 1)
    }
  }
複製代碼

dispatch

dispatch首先會檢查參數,隨後會執行currentReducer(currentState, action),而這個方法實際就是combineReducers

function dispatch(action) {
    // 若是dispatch的參數不是action
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    // action必須得有type屬性,若是沒有會報錯
    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
      // 執行reducer 遍歷過濾後的reducer,隨後依次賦值到state當中
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 獲取當前的監聽器
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      // 依次執行監聽器回調
      listener()
    }

    // dispatch默認返回action
    return action
  }
複製代碼

replaceReducer

/** * Replaces the reducer currently used by the store to calculate the state. * * You might need this if your app implements code splitting and you want to * load some of the reducers dynamically. You might also need this if you * implement a hot reloading mechanism for Redux. * * @param {Function} nextReducer The reducer for the store to use instead. * @returns {void} */
  /** * 替換reducer * * 動態替換原有的reducer */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 將reducer賦值
    currentReducer = nextReducer
    // 發送一個dispatch 隨後重置store
    dispatch({ type: ActionTypes.REPLACE })
  }
複製代碼

observable

這裏不談太多observable

這裏有個使用例子

const state$ = store[Symbol.observable]();
const subscription = state$.subscribe({
  next: function(x) {
     console.log(x);
   }
 });
 subscription.unsubscribe();
複製代碼
/** * 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 */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /** * The minimal observable subscription method. * @param {Object} observer Any object that can be used as an observer. * The observer object should have a `next` method. * @returns {subscription} An object with an `unsubscribe` method that can * be used to unsubscribe the observable from the store, and prevent further * emission of values from the observable. */
      /** * @param {Object} 任何對象均可以當作observer * observer應該有一個`next`方法 * @returns {subscription} 一個對象,它有`unsubscribe`方法可以 * 用來從store中unsubscribe observable */
      subscribe(observer) {
        // observer必須是一個非空的object
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }
複製代碼

bindActionCreators

在講這個方法前,先看下文檔對它的使用說明

Example

TodoActionCreators.js

咱們在文件中建立了2個普通的action。

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
​
export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}
複製代碼

SomeComponent.js

import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'import * as TodoActionCreators from './TodoActionCreators'
console.log(TodoActionCreators)
// {
// addTodo: Function,
// removeTodo: Function
// }class TodoListContainer extends Component {
  constructor(props) {
    super(props)
​
    const { dispatch } = props
​
    // Here's a good use case for bindActionCreators:
    // You want a child component to be completely unaware of Redux.
    // We create bound versions of these functions now so we can
    // pass them down to our child later.this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
    console.log(this.boundActionCreators)
    // {
    // addTodo: Function,
    // removeTodo: Function
    // }
  }
​
  componentDidMount() {
    // Injected by react-redux:
    let { dispatch } = this.props
​
    // Note: this won't work:
    // TodoActionCreators.addTodo('Use Redux')// You're just calling a function that creates an action.
    // You must dispatch the action, too!// This will work:
    let action = TodoActionCreators.addTodo('Use Redux')
    dispatch(action)
  }
​
  render() {
    // Injected by react-redux:
    let { todos } = this.props
​
    return <TodoList todos={todos} {...this.boundActionCreators} />
​
    // An alternative to bindActionCreators is to pass
    // just the dispatch function down, but then your child component
    // needs to import action creators and know about them.
​
    // return <TodoList todos={todos} dispatch={dispatch} />
  }
}
​
export default connect(state => ({ todos: state.todos }))(TodoListContainer)
複製代碼

bindActionCreators.js

咱們接下來來看它的源碼

/** * * 在看`bindActionCreator`方法以前能夠先看`bindActionCreators`方法 * * @param {Function} actionCreator 實際就是 action * */
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    // 返回的是dispath
    return dispatch(actionCreator.apply(this, arguments))
  }
}

// actionCreators是一個包含衆多actions的對象
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    // actionCreators是函數就表明他是單一的action方法
    return bindActionCreator(actionCreators, dispatch)
  }

  // actionCreator若是不是object 或者它是空的則報錯
  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"?`
    )
  }
  // 將action的keys遍歷出來
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    // 每一個action的key
    const key = keys[i]
    // 將action取出 這是一個方法
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      // bindActionCreator返回的是dispatch的返回值
      // 實際是action 因此boundActionCreators是一個dispatch function的對象
      // 同時若是key相同會被覆蓋
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

複製代碼

使用bindActionCreators實際能夠建立一個充滿dispatch方法的對象。而後能夠將這個對象傳遞子組件來使用。

總結

看完源碼後咱們大體瞭解到爲何reducer必須是functionstore中的state爲何會建立和reducer相應的對象名的state,爲何只能經過dispatch來對store進行操做。另外redux的一個核心不可變性,redux自己並不能保證。因此咱們在本身寫的reducer當中必需要保證不能改變store原有的對象,必須得從新建立。

相關文章
相關標籤/搜索