Analyze Redux source code step by step

preface

Recently, I encountered some complicated data processing in the project, and realized how important a good data layer design is to the stability and maintainability of a project. I wanted to summarize the current data management approach in the form of source code analysis, and preferred Redux. Redux’s official documentation provides an idea of its design. cn.redux.js.org/.

This article is saved on my Github welcome fork or star github.com/yangfan0095…

Redux source entry index.js

We can see that Redux exports the following five modules. There are five JS files.

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'.export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}

Copy the code
  • CreateStore Creates a Store

  • CombineReducers Role: Combine the reducer

  • BindActionCreators works by converting an object with a value of a different ActionCreator into an object with a key of the same name

  • What applyMiddleware does: Extends Redux introduction with custom middleware

  • Compose: compose multiple functions from right to left

One of the main files is createStore.

createStore

CreateStore The primary purpose of this method is to create a store with an initialized state tree. The state tree can only trigger reducer changes through the Dispatch API provided by createStore.

The source code is as follows:

import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

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

  if(typeof reducer ! = ='function') {
    throw new Error('Expected the reducer to be a function.')}...return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
  
Copy the code

The method has three parameters reducer, preloadedState, enhancer. Reducer is a method combination returned after combineReducers is used to generate store. This method input state and action to return the latest state tree, which is used to update state. PreloadedState is used to initialize state data, enhancer is a higher-order function used to extend store functionality, for example applyMiddleware is an enhancer function.

Look at the source code, first js functions pass parameters. The source code determines the type of the second argument, if it is function then it is not initState. So replace the second parameter with enhancer. This improved our development experience.

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

Copy the code

For the enhancer operation, if the enhancer operation exists, then the following statement.

if(typeof enhancer ! = ='undefined') {
    if(typeof enhancer ! = ='function') {
      throw new Error('Expected the enhancer to be a function.')}return enhancer(createStore)(reducer, preloadedState)
  }
  
Copy the code

I found an example on the Internet. First enhancer is a function similar to the following structure. The function returns an anonymous function. CreateStore => (Reducer, initialState, enhancer) => {… }) then execute the method directly.

export default function autoLogger() {
  return createStore => (reducer, initialState, enhancer) => {
    const store = createStore(reducer, initialState, enhancer)
    function dispatch(action) {
      console.log(`dispatch an action: ${JSON.stringify(action)}`);
      const result = store.dispatch(action);
      const newState = store.getState();
      console.log(`current state: ${JSON.stringify(newState)}`);
      return result;
    }
    return{... store, dispatch} } }Copy the code

To summarize, with this enhancer we can change the behavior of the Dispatch. This example allows users to access our new enhancer dispatch by overwriting the dispatch in the store by defining a new dispatch in the enhancer method. There’s also a method called applyMiddleware, which we’ll talk about later.

And then we see here, we’ve configured some variables, and we’re going to initialize preloadedState to currentState. These variables are actually a closure that holds some global data.

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false.Copy the code

The subscribe method

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

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      isSubscribed = false

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

The subscribe method implements a subscribe listener. The listener is a callback function that is called after each dispatch execution, as shown in the following code:

function dispatch(action){
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }
    ...
    
Copy the code

The subscription function returns an unsubscribe function, and the listener is placed in the list of current subscriber callbacks. First the Combination and our current incoming renducer update the latest state tree, and then the listener is called to update the caller’s data. In React this call is typically used to update the current component state. React we use React -redux to connect to redux. To learn more you can view the source github.com/reactjs/rea… .

The source code is highly encapsulated. Here is a rough example of how the high-order component Connect subscribs to Redux and updates the component state in React-Redux.

static contextTypes = {
    store: PropTypes.object
  }

  constructor () {
    super()
    this.state = { themeColor: ' '}} // Add a subscription before the component is mountedcomponentWillMount () {
    this._updateThemeColor()
    store.subscribe(() => this._updateThemeColor())
  }

  _updateThemeColor () {
    const { store } = this.context
    const state = store.getState()
    this.setState({ themeColor: state.themeColor })
  }

Copy the code

We call store.subscribe(() => this._updateThemecolor ()) to subscribe store () => this._updatethemecolor (). When we dispatch a listener we are actually executing this.setState({themeColor: state.themecolor}) to update the state of the corresponding state tree for the current component.

The Listener is saved to the nextListeners before each dispatch, acting as a snapshot. If you receive a subscription or contact a subscription while you are executing the Listener function, the latter will not take effect immediately, but the next call to Dispatch will use the latest list of subscribers, called nextListeners. The latest subscriber snapshot nextListeners are assigned to currentListeners when dispatch is called. Here’s a blog post devoted to this topic

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

EnsureCanMutateNextListeners function

About ensureCanMutateNextListeners function role, I saw a lot of information, but did not find a good explanation. The general effect is that some scenarios may cause duplicate listeners to be added, resulting in two identical handlers in the current subscriber list. The role of ensureCanMutateNextListeners happened in order to avoid this kind of phenomenon.

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

When we append a subscription in the following manner, a duplicate subscription is caused when we perform dispatch. Concrete examples we can see this link: the React Redux source function ensureCanMutateNextListeners?

const store = createStore(someReducer);

function doSubscribe() {
  store.subscribe(doSubscribe);
}
doSubscribe(); 

Copy the code

Dispatch method

Let’s look at the Dispatch function, which issues an action that triggers the Reducer to change the current state tree and then executes a listener to update the component state. Here is the internal Dispatch method, and the action passed in is a naive Action object. Contains a state and type attribute. Enhancer can be enhanced if you need more action support. To support promises, you can use Redux-Promise, which is essentially middleware. The action is processed internally and eventually passed a naive Action object into the Dispatch method. We’ll cover this topic again in applyMiddleware below.

   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
  }
  
Copy the code

First dispatch determines whether the action type is valid through a method introduced by a third party. The currentState and action are then passed into the currentReducer function, which returns the latest state assignment to currentState. All updated Listeners are then triggered to update state

 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()
    }
    
Copy the code

ReplaceReducer method

The replaceReducer method is used to dynamically update the current currentReducer. This method replaces the current currentReducer by exposing the replaceReducer API. Then dispatch({type: actiontypes.init}) is essentially an initialized createStore operation. Dispatch ({type: actiontypes.init}) is an initialized action {type: Actiontypes.init} all the current reducers are distributed, and the Reducer returns their initial values, thus generating an initialized state tree.

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

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }
  
Copy the code

GetState method

Returns the current state tree

  function getState() {
    return currentState
  }
  
Copy the code

Observables method

Observables are exposed via private properties for internal use only. This function returns a subscribe method that can be used to observe changes in the state of the smallest unit. The observer argument to this method is an object with a property of type Next (Function).

The outerSubscribe function first assigns the SUBSCRIBE method from createStore to outerSubscribe. In the return method, we first define the function observeState and then pass it to outeSubscribe. It actually encapsulates a Linster reference to SUBSCRIBE. When the message is distributed, the Linster is launched, and the next method calls observer.next(getState()) to get the current state

    function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }
    observeState()
    const unsubscribe = outerSubscribe(observeState)
        
Copy the code

More information about Observables can be found at github.com/tc39/propos…

function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if(typeof observer ! = ='object') {
          thr[$$observable]: observableow new TypeError('Expected the observer to be an object.')}function observeState() {
          if(observer.next) {observer.next(getState())}} // Get the observed status and return a method to unsubscribe. observeState() const unsubscribe = outerSubscribe(observeState)return { unsubscribe }
      },

      [$$observable] () {return this
      }
    }
  }
  
  
Copy the code

That’s it for createStore, which is one of the core methods in Redux. It provides a complete observing subscription method API for third parties.

bindActionCreators

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
}

function bindActionCreator(actionCreator, dispatch) {
  return(... args) => dispatch(actionCreator(... args)) }Copy the code

We can focus on the bindActionCreator method, which is very simple to pass in a raw Action object, and the Dispatch method. Returns a method to distribute the action (… args) => dispatch(actionCreator(… The args)). Our original action object looks like this

 export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
Copy the code

For example, when we obtain the updated state value in React, we need to manually execute the reducer (listener) function through dispatch to update the state number

dispatch(addTodo)
Copy the code

This is a direct encapsulation of our manual dispatch method. The action actually becomes (… args) => dispatch(actionCreator(… Args)) we automate dispatch when we execute. The bindActionCreators method iterates through our action methods for exporting the corresponding file, executing bindActionCreator separately and returning an updated action set, boundActionCreators.

combineReducers

The job is to merge multiple reducers into a reducer and return a combination method. Combination is the Reducer passed in by the createStore operation. This method takes a state and an action returns the latest number of states

For example, currentState = currentReducer(currentState, action) is called during dispatch to update the currentState tree. The subscriber callback function Listener is then executed to update the state of the subscriber (such as the React component).

export default functionCombineReducers (reducers) {Const reducerKeys = object. keys(reducers) // Verify the validity and then obtain all current Reducer objects 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)

  let unexpectedKeyCache
  if(process.env.NODE_ENV ! = ='production') {unexpectedKeyCache = {}} // Call assertReducerShape to check the legitimacy of the reducer functionletShapeAssertionError try {assertReducerShape(finalReducers)} Catch (e) {shapeAssertionError = e} // Returns a combination method This method passes in state and action to return a new state treereturn 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
  }
}

Copy the code

Compose and applyMiddleware functions

Compose source code:

export default functioncompose(... funcs) {if (funcs.length === 0) {
    return arg => arg
  }

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

  returnfuncs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code

Funcs.reduce ((a, b) => (… args) => a(b(… Args)))compose compose middleware functions from right to left. Let’s look at a common middleware example, Redux-Thunk

redux-logger

let logger = ({ dispatch, getState }) => next => action => {
    console.log('state before next:', getState())
    let result = next(action)
    console.log('state before next:', getState())
    return result
}

let thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState)
    }
    return next(action)
}
Copy the code

The middleware format is input dispatch and getState outputs next

letmiddleware = ({ dispatch, getState }) => next => action => { ... / / operationreturn next(action)
}
Copy the code

The compose function inputs an array of middleware functions and iterates through reduce to middleware1(MIDDLEware2 (Middleware3 (… Args))). Because of the JS pass-value invocation, each middleware has actually been executed once before passing in the second middleware. So the args passed by the second middleware is the next method of the first middleware return. So compose returns a function that can be simplified to function returnComposeFunc = Middleware (next). Now let’s look at applyMiddleware

export default functionapplyMiddleware(... middlewares) {return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    letchain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch)return {
      ...store,
      dispatch
    }
  }
}

Copy the code

ApplyMiddleware here is an enhancer method used to enhance store dispatch functionality, which is used to merge middleware. The first step is to use closures to pass each middleware the required getState and Dispatch.

const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
chain = middlewares.map(middleware => middleware(middlewareAPI))
Copy the code

All middleware is then pushed into an array chain. The compose function is then executed. Execute the compose function compose(… Chain)(Store.dispatch) compose returns a returnComposeFunc. We pass in a store.dispatch and it returns a method. And that method is our new Dispatch method.

 action => {
    if (typeof action === 'function') {
        return action(dispatch, getState)
    }
    return next(action)
}
Copy the code

I have to say that the code here is only a few lines, but it is so worth thinking about, very well written!

Redux: Redux: Redux: Redux: Redux: Redux: Redux React-redux also has a lot to say about it, and I’ll write an analysis of it later.

Welcome to clap brick at last

The resources

  • Chinese Official Documents
  • Redux middleware,
  • www.cnblogs.com/cloud-/p/72…
  • Alloyteam explores the little secrets of React-Redux
  • Github.com/evgenyrodio…
  • redux-thunk
  • Ruan Yifeng standard ES6 Generator function of asynchronous applications in the thunk function and our middleware function ideas are similar