preface

React: UI rendering of components Redux: data state management React-redux: associate React and Redux

Redux

The basic concept

You can also check out redux’s Related Chinese documentation first

  • The three principles

Single data source: The state of the entire application is stored in an object for easy debugging and data management.

State is read-only: the only way to change State is to trigger an action that stores the data to be updated.

Use a pure function to perform the modification: This pure function is the Reducer. The reducer receives the data from the action and returns the relevant state according to the business scenario.

  • action

Action is the only source of store data. You need to include the Type field in the action, and you can define the rest of the structure based on your project and business needs.

// Create a function structure for an action that returns the action object
import * as ActionTypes from '.. /actionType';

/ * * *@func
 * @desc Update user information *@param data 
 */
export function updateUserInfo(data: { [key: string] :any }) {
    return {
        type: ActionTypes.USERINFO_UPDATE,
        data
    };
};

Copy the code
  • reducer

Reducer is a pure function that receives the old state and action and returns the new state.

Do not do these things in reducer: (1) modify incoming parameters (2) perform side effects such as API requests and route redirects (3) call impure functions such as date.now () or math.random () Because reducer must be pure. As long as the arguments passed in are the same, the next state returned must be the same. No special cases, no side effects, no API requests, no variable changes, just perform calculations.

// Reducer is the map-reducer function

import * as ActionTypes from '.. /actionType';
import { combineReducers } from 'redux';

interface IUserinfoAction {
    type: string;
    [key: string] :any;
}

const initialState = {};

function userInfo(state = initialState, action: IUserinfoAction) {
    switch(action.type) {
        case ActionTypes.USERINFO_UPDATE:
            return action.data;
        default:
            returnstate; }}const rootReducer = combineReducers({
    userInfo
});

export default rootReducer;
Copy the code
  • store

Store is an object with the following attributes: getState, Dispatch, Subscribe, and replaceReducer. (1) getState is used to getState (2) dispatch(action) is used to update state (3) subscribe(listener) is used to register listeners, and the function returned can be used to unregister listeners

  • The simple Case of Redux

Project file Directory:



// actionType.ts 
// Define the action type
export const USERINFO_UPDATE = 'USERINFO_UPDATE';
export const SEARCHBOOK_UPDATE = 'SEARCHBOOK_UPDATE';
export const CHAPTER_UPDATE = 'CHAPTER_UPDATE';
Copy the code
// user/action.ts
import * as ActionTypes from '.. /actionType';

/ * * *@func
 * @desc Update user information *@param data 
 */
export function updateUserInfo(data: { [key: string] :any }) {
    return {
        type: ActionTypes.USERINFO_UPDATE,
        data
    };
};
Copy the code
// user/reducer.ts
import * as ActionTypes from '.. /actionType';
import { combineReducers } from 'redux';

interface IUserinfoAction {
    type: string;
    [key: string] :any;
}

const initialState = {};

function userInfo(state = initialState, action: IUserinfoAction) {
    switch(action.type) {
        case ActionTypes.USERINFO_UPDATE:
            return action.data;
        default:
            returnstate; }}const rootReducer = combineReducers({
    userInfo
});

export default rootReducer;
Copy the code
// rootReducer.ts
Merge all reducer files
import { combineReducers } from 'redux';
import userReducer from './user/reducer';
const rootReducer = combineReducers({
    userReducer,
    / *... The rest of the reducer... * /
});

export default rootReducer;
Copy the code
// index.ts
// 
import rootReducer from './rootReducer';
import { createStore, applyMiddleware } from 'redux';

const initState: { [key: string] :any } = {/ *... The value of the initialization state... * /};
export const store = createStore(rootReducer, initState, applyMiddleware(
    / *... Middleware, enhanced Dispatch... * /
));
Copy the code
// index. TSX project index position
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { store } from 'store/index';

ReactDOM.render(
    <Provider store= {store}>
        <App />
    </Provider>.document.getElementById('root')); serviceWorker.unregister();Copy the code

The source code parsing

createStore.js

import $$observable from 'symbol-observable'

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

export default function createStore(reducer, preloadedState, enhancer) {
  / *... * /

  / *... * /

  /* check whether enhancer is function, Return enhancer(createStore)(Reducer, preloadedState) Enhancer = applyMiddleware Middleware, enhanced Dispatch... * /) * /if (typeofenhancer ! = ='undefined') {
    if (typeofenhancer ! = ='function') {
      throw new Error('Expected the enhancer to be a function.')}return enhancer(createStore)(reducer, preloadedState)
  }

  / *... * /

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

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

  /* Returns the latest status value */
  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
  }

  /* Listeners are passed in, stored in the nextListeners container and returned with an unsubscribe function that unloads the listeners. Closure is used to find the index value of the listener and remove it from the nextListeners container */
  function subscribe(listener) {
    if (typeoflistener ! = ='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#subscribelistener for more details.')}let isSubscribed = true

    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#subscribelistener for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null}}CurrentReducer (reducer) currentReducer (reducer) currentReducer (Reducer) currentReducer (Reducer) currentReducer (Reducer) And then we assign the state to currentState and currentState is the getState method that returns the value that we need which is the most recent state and loops through the listeners that are in there. The data on the listeners come from a subscribe, a subscribe. * /
  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
  }

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

    currentReducer = nextReducer

    dispatch({ type: ActionTypes.REPLACE })
  }

  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeofobserver ! = ='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
      }
    }
  }

  dispatch({ type: ActionTypes.INIT })

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

Export const store = createStore(rootReducer, initState, applyMiddleware(/*… * /)); RootReducer represents the reducer after the reducer, initState is the default initial state, and applyMiddleware(/*… */) stands for enhancer, which takes middleware (for example: Redux-thunk) createStore Specifies whether enhancer is a function. If yes, return enhancer(createStore)(Reducer state). Enhancer is applyMiddleware(/*… Middleware, enhanced Dispatch… */) The createStore provides a getState method that returns the latest status value. The createStore provides a subscribe method, which is passed to the listener function and stored in the nextListeners container. And returns an unsubscribe function to unload the subscription function. Each time the createStore provides an internal dispatch method, the listener’s index value can be removed from the nextListeners by using closures. CurrentReducer is the reducer collection that was originally passed in. CurrentReducer returns the latest state, And then I assign the state to currentState and currentState is what the getState method returns which is the most recent state. The listeners are also iterated over, executing the listeners. Listeners derive data from the execution of the SUBSCRIBE method

applyMiddleware.js

import compose from './compose'

export default function applyMiddleware(. middlewares) {
  return createStore= > (. args) = > {
    conststore = 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
    }
  }
}
Copy the code

ApplyMiddleware returns a function to createStore, which also returns an anonymous function to reducer and preloadedState. This anonymous function calls createStore to create a store. It also declares a variable middlewareAPI that stores getState and Dispatch. The getState user gets the latest state, The dispatch is for the enhanced dispatch returned later through the middleware map passed in.

combineReducers.js

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 (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  
  let unexpectedKeyCache
  / *... * /

  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    / *... * /

    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 } hasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
Copy the code

CombineReducers merges all the reducer files. All keys passed in from the Reducer will be obtained first, and then the reducer will be looped through and mounted to an object called finalReducers. It then returns a method called combination. The combination function is to run to produce state, which takes an initial state value and an action as parameters. Inside is the loop through finalReducers. Mount execution generation state to nextState. Finally, it determines whether the state has changed, and returns the latest state if it has, or the old state otherwise.

compose.js

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

Compose (a, b, c)(/*.. Parameter.. */) => a(b(c(/*.. Parameters… * /)))

React-Redux

A simple case

// index. TSX project index position
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { store } from 'store/index';

ReactDOM.render(
    <Provider store= {store}>
        <App />
    </Provider>.document.getElementById('root')); serviceWorker.unregister();Copy the code
// book.tsx
import React from 'react';
import {connect} from 'react-redux';
import { bindActionCreators } from 'redux';
/ *... The rest of the import... * /

class BookContainer extends React.PureComponent<IProps.IState> {
	/ *... Business code... * /
}

/* You can map the state data of redux to the props of the component */
function mapStateToProps(state: IReduxState) {
    return {
    	searchContent: state.searchContent
    };
}

/* Map action to props of the component */
function mapDispatchToProps(dispatch: any) {
    return {
        updateChapterMaterial: bindActionCreators(updateChapterMaterial, dispatch)
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(BookContainer);
Copy the code

provider

function Provider({ store, context, children }) {
  const contextValue = useMemo(() = > {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription
    }
  }, [store])

  const previousState = useMemo(() = > store.getState(), [store])

  useEffect(() = > {
    const { subscription } = contextValue
    subscription.trySubscribe()

    if(previousState ! == store.getState()) { subscription.notifyNestedSubs() }return () = > {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
Copy the code

The Provider component receives store objects, context objects, and children React node elements. There is an internal variable called contextValue, and with useMemo, whenever a dependency store changes, a subscription listener is created, returning store and Subscription. When the contextValue and previousState change, the code in useEffect is executed. This is actually the method that executes the subscription. Note here that the value of the Provider is store and subscription. The purpose is to provide for the CONNECT component. Subscription is used to update connect and Provider components.

connect

class BookContainer extends React.Component<IProps.IState> {
	/ *... Business code... * /
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(BookContainer);
Copy the code

Code like this: Connect is a high-level component that receives the mapStateToProps and mapDispatchToProps parameters. The mapStateToProps is used to map a specific state to the component’s props. MapDispatchToProps maps Dispatch (Action) to props, internally listening for changes in the Store of Redux, and when state changes, all connected components render once.

Match, initMapStateToProps, initMapDispatchToProps, and initMergeProps

import defaultMapDispatchToPropsFactories from './mapDispatchToProps'
import defaultMapStateToPropsFactories from './mapStateToProps'
import defaultMergePropsFactories from './mergeProps'
/ *... Other import modules... * /

function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return (dispatch, options) = > {
    throw new Error(
      `Invalid value of type The ${typeof arg} for ${name} argument when connecting component ${ options.wrappedComponentName }. `)}}export function createConnect({
  connectHOC = connectAdvanced,
  /*......*/} = {}){
  /*......*/
  const initMapStateToProps = match(
    mapStateToProps,
    mapStateToPropsFactories,
    'mapStateToProps'
  )

  const initMapDispatchToProps = match(
    mapDispatchToProps,
    mapDispatchToPropsFactories,
    'mapDispatchToProps'
  )
    
  const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')}Copy the code

MapDispatchToProps, mapStateToProps, and mergeProps are initialized with a match function. (2) Match executes the factories method in a loop. (3) The factories come from an array of results returned by mapStateToProps and mapDispatchToProps. The member of the array is either undefined, or the structure of the member is (dispatch, options) => (nextState, nextOwnProps) => nextFinalProps. (4) The result obtained by match (initMapStateToProps, initMapDispatchToProps, initMergeProps) will be passed into connectAdvanced for use. The connectAdvanced function calculates the props needed for the final component based on the parameters passed in and the Store object.

mapStateToProps.js

// mapStateToProps.js
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

export function whenMapStateToPropsIsFunction(mapStateToProps) {
  return typeof mapStateToProps === 'function'
    ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
    : undefined
}

export function whenMapStateToPropsIsMissing(mapStateToProps) {
  return! mapStateToProps ? wrapMapToPropsConstant(() = > ({})) : undefined
}

export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
Copy the code

The above code: whenMapStateToPropsIsFunction: when we pass mapStateToProps is a method of value will return the result. WhenMapStateToPropsIsMissing: when we incoming mapStateToProps converted to false, will return to a default result. The two result values are described in the code below.

// wrapMapToProps.js
export function wrapMapToPropsConstant(getConstant) {
  return function initConstantSelector(dispatch, options) {
    const constant = getConstant(dispatch, options)

    function constantSelector() {
      return constant
    }
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
}

export function getDependsOnOwnProps(mapToProps) {
  returnmapToProps.dependsOnOwnProps ! = =null&& mapToProps.dependsOnOwnProps ! = =undefined
    ? Boolean(mapToProps.dependsOnOwnProps) : mapToProps.length ! = =1
}

export function wrapMapToPropsFunc(mapToProps, methodName) {
  return function initProxySelector(dispatch, { displayName }) {
    const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
      return proxy.dependsOnOwnProps
        ? proxy.mapToProps(stateOrDispatch, ownProps)
        : proxy.mapToProps(stateOrDispatch)
    }

    // allow detectFactoryAndVerify to get ownProps
    proxy.dependsOnOwnProps = true

    proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
      proxy.mapToProps = mapToProps
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      let props = proxy(stateOrDispatch, ownProps)

      if (typeof props === 'function') {
        proxy.mapToProps = props
        proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
        props = proxy(stateOrDispatch, ownProps)
      }

      if(process.env.NODE_ENV ! = ='production')
        verifyPlainObject(props, displayName, methodName)

      return props
    }

    return proxy
  }
}
Copy the code

Code like this: (1) wrapMapToPropsFunc => (nextState, nextOwnProps) => nextFinalProps, Return function initProxySelector(dispatch, {displayName}) corresponds to (dispatch, options), Function mapToPropsProxy(stateOrDispatch, ownProps) corresponds to nextState (nextOwnProps), and proxy.mapToProps corresponds to nextFinalProps. DependsOnOwnProps = true on first run DetectFactoryAndVerify gets the second parameter (ownProps) when it is running, and the second time when it is running, proxy. MapToProps and denpendsOnOwnProps are calculated. (3) getDependsOnOwnProps calculates the value of denpendsOnOwnProps. The purpose of this method is to calculate whether the mapToProps needs to be used. If mapToProps length! If mapToProps. Length === 1, only the stateOrDispatch needs to be calculated. (4) The wrapMapToPropsConstant method is used to calculate that when the passed mapStateToProps is null, the constantSelector method is returned. Inside this method, constant is returned as an empty object. Reference function whenMapStateToPropsIsMissing (mapStateToProps) {return! mapStateToProps ? WrapMapToPropsConstant (() => ({})) : undefined, so ()=>{} represents getConstant. DependsOnOwnProps is set to false because mapStateToProps is not passed in

ownProps

class Demo extends React.Component<IProps.IState> {
 constructor() {
 	/ *... * /
 }
  
  render(){
    return <div>User name: {this.props.user.name}</div>}}const mapStateToProps = (state, ownProps) = > {
  return {
    user: {
    	id: _.find(state.userList, {id: ownProps.userId})
    }
  }
}

export default connect(
  mapStateToProps
)(Demo);
Copy the code

If you are wondering what ownProps is, ownProps is the props of the business component itself. If a userList is maintained in the Store, but the business component only cares about one user. When state changes, or ownProps changes, mapStateToProps is called to calculate a new stateProps.

mapDispatchToProps.js

import { bindActionCreators } from 'redux'
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
  return typeof mapDispatchToProps === 'function'
    ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
    : undefined
}

export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
  return! mapDispatchToProps ? wrapMapToPropsConstant((dispatch) = > ({ dispatch }))
    : undefined
}

export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
  return mapDispatchToProps && typeof mapDispatchToProps === 'object'
    ? wrapMapToPropsConstant((dispatch) = >
        bindActionCreators(mapDispatchToProps, dispatch)
      )
    : undefined
}

export default [
  whenMapDispatchToPropsIsFunction,
  whenMapDispatchToPropsIsMissing,
  whenMapDispatchToPropsIsObject,
]
Copy the code

Code like this: If the type of mapDispatchToProps is null, the internal logic is wrapMapToPropsFunc of wrapMapToProps Call wrapMapToPropsConstant and pass (dispatch)=>({dispatch}) by default, So we can call this.props. Dispatch to the store dispatchprops ourselves from within the business component. Call bindActionCreators, which is ultimately the combined props

mergeProps.js

import verifyPlainObject from '.. /utils/verifyPlainObject'

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return{... ownProps, ... stateProps, ... dispatchProps } }export function wrapMergePropsFunc(mergeProps) {
  return function initMergePropsProxy(dispatch, { displayName, pure, areMergedPropsEqual }) {
    let hasRunOnce = false
    let mergedProps

    return function mergePropsProxy(stateProps, dispatchProps, ownProps) {
      const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps)

      if (hasRunOnce) {
        if(! pure || ! areMergedPropsEqual(nextMergedProps, mergedProps)) mergedProps = nextMergedProps }else {
        hasRunOnce = true
        mergedProps = nextMergedProps

        if(process.env.NODE_ENV ! = ='production')
          verifyPlainObject(mergedProps, displayName, 'mergeProps')}return mergedProps
    }
  }
}

export function whenMergePropsIsFunction(mergeProps) {
  return typeof mergeProps === 'function'
    ? wrapMergePropsFunc(mergeProps)
    : undefined
}

export function whenMergePropsIsOmitted(mergeProps) {
  return! mergeProps ?() = > defaultMergeProps : undefined
}

export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]
Copy the code

The purpose is to combine stateProps, dispatchProps, and ownProps.

connect.js

// connect.js
import connectAdvanced from '.. /components/connectAdvanced'
/*......*/
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ... extraOptions } = {}) {
    const initMapStateToProps = match(
      mapStateToProps,
      mapStateToPropsFactories,
      'mapStateToProps'
    )
    const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
      methodName: 'connect'.getDisplayName: name= > `Connect(${name}) `.shouldHandleStateChanges: Boolean(mapStateToProps), initMapStateToProps, initMapDispatchToProps, initMergeProps, pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, ... extraOptions }) } }export default /*#__PURE__*/ createConnect()
Copy the code

Running createConnect() returns the connect method. When we use connect and execute, a higher-order component, connectHOC, is returned (connectHOC comes from the connectAdvanced parameter passed in the createConnect method).

selectorFactory.js

export function impureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}

export function pureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps

  function handleFirstCall(firstState, firstOwnProps) {
    state = firstState
    ownProps = firstOwnProps
    stateProps = mapStateToProps(state, ownProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    hasRunAtLeastOnce = true
    return mergedProps
  }

  function handleNewPropsAndNewState() {
    stateProps = mapStateToProps(state, ownProps)

    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewProps() {
    if (mapStateToProps.dependsOnOwnProps)
      stateProps = mapStateToProps(state, ownProps)

    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewState() {
    const nextStateProps = mapStateToProps(state, ownProps)
    conststatePropsChanged = ! areStatePropsEqual(nextStateProps, stateProps) stateProps = nextStatePropsif (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

    return mergedProps
  }

  function handleSubsequentCalls(nextState, nextOwnProps) {
    constpropsChanged = ! areOwnPropsEqual(nextOwnProps, ownProps)conststateChanged = ! areStatesEqual(nextState, state) state = nextState ownProps = nextOwnPropsif (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}

export default function finalPropsSelectorFactory(dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ... options }) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  if(process.env.NODE_ENV ! = ='production') {
    verifySubselectors(
      mapStateToProps,
      mapDispatchToProps,
      mergeProps,
      options.displayName
    )
  }

  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
Copy the code

(1) Irrelevant changes in the Store are prevented by the selectorFactory. State, for example: {userList: [/ *… * /], bookList: [/ *… * /]}. A business component just needs a part of the state, the userList. At some point the value of the bookList property in state changes, and store.subscribe listens for state changes. However, the userList that the business component cares about has not changed, so the current business component should not update if it is in pure mode, where the processing logic is in selectorFactory.js. (2) side notice options. Pure, when it is true, it is pureFinalPropsSelectorFactory selectorFactory value, For impureFinalPropsSelectorFactory otherwise. In non-pure mode, this is the combined props. In Pure mode, the first run executes handleSubsequentCalls, and the second and subsequent handleFirstCall methods. (3) In the handleFirstCall method, ownProps, stateProps, dispatchProps are combined, and hasRunAtLeastOnce is set to true. (4) handleSubsequentCalls compare whether props, state, or both have changed. If only the props changes, execute handleNewProps (this method is internally optimized so that the mapStateToProps and mapDispatchToProps are not re-evaluated if they do not depend on props). If the state changes, execute handleNewState (recalculate the stateProps and compare the mergedProps strictly, recalculate the mergedProps if it changed, and return the old mergedProps if it did not change. That is, if the value of the state property associated with the business component does not change, the component is not updated. . The state and props are changing, handleNewPropsAndNewState execution. (5) selectorFactory js use dependency injection, to the top function finalPropsSelectorFactory elements are in through injection parameters, need to look up two levels to find the source. (6) Dependency injection (DI) : for example, classA uses some attributes of class B, and then there is A special container to instantiate class B. When classA needs to use attributes of B, it simply asks the container for instances of B. The purpose is to reduce the coupling between classes and eliminate the need for multiple new instances for later maintenance and management.

connectAdvanced.js

// connectAdvanced.js
import Subscription from '.. /utils/Subscription'

/* shouldHandleStateChanges should be executed when store changes are made to ensure that state changes are processed or not. CheckForUpdates () is executed by default when you initialize the subscribeUpdates that are not executed. CheckForUpdates () is used to obtain the latest status */
function subscribeUpdates(shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, lastChildProps, renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch) {
  if(! shouldHandleStateChanges)return

  let didUnsubscribe = false
  let lastThrownError = null

  const checkForUpdates = () = > {
    if (didUnsubscribe) {
      return
    }

    const latestStoreState = store.getState()

    let newChildProps, error
    try {
      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } catch (e) {
      error = e
      lastThrownError = e
    }

    if(! error) { lastThrownError =null
    }

    if (newChildProps === lastChildProps.current) {
      if(! renderIsScheduled.current) { notifyNestedSubs() } }else {
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true

      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED'.payload: {
          error,
        },
      })
    }
  }

  subscription.onStateChange = checkForUpdates
  subscription.trySubscribe()

  checkForUpdates()

  const unsubscribeWrapper = () = > {
    didUnsubscribe = true
    subscription.tryUnsubscribe()
    subscription.onStateChange = null

    if (lastThrownError) {
      throw lastThrownError
    }
  }

  return unsubscribeWrapper
}

// Call selectorFactory,
function createChildSelector(store) {
  return selectorFactory(store.dispatch, selectorFactoryOptions)
}

// Connect core code
function ConnectFunction(props) {
  const [
    propsContext,
    reactReduxForwardedRef,
    wrapperProps,
  ] = useMemo(() = > {
    const{ reactReduxForwardedRef, ... wrapperProps } = propsreturn [props.context, reactReduxForwardedRef, wrapperProps]
  }, [props])

  const ContextToUse = useMemo(() = > {
    return propsContext &&
      propsContext.Consumer &&
      isContextConsumer(<propsContext.Consumer />)? propsContext : Context }, [propsContext, Context])const contextValue = useContext(ContextToUse)

  const didStoreComeFromProps =
    Boolean(props.store) &&
    Boolean(props.store.getState) &&
    Boolean(props.store.dispatch)
  const didStoreComeFromContext =
    Boolean(contextValue) && Boolean(contextValue.store)

  if( process.env.NODE_ENV ! = ='production'&&! didStoreComeFromProps && ! didStoreComeFromContext ) {throw new Error(
      `Could not find "store" in the context of ` +
        `"${displayName}". Either wrap the root component in a <Provider>, ` +
        `or pass a custom React context provider to <Provider> and the corresponding ` +
        `React context consumer to ${displayName} in connect options.`)}const store = didStoreComeFromProps ? props.store : contextValue.store

  const childPropsSelector = useMemo(() = > {
    return createChildSelector(store)
  }, [store])

  const [subscription, notifyNestedSubs] = useMemo(() = > {
    if(! shouldHandleStateChanges)return NO_SUBSCRIPTION_ARRAY

    const subscription = new Subscription(
      store,
      didStoreComeFromProps ? null : contextValue.subscription
    )

    const notifyNestedSubs = subscription.notifyNestedSubs.bind(
      subscription
    )

    return [subscription, notifyNestedSubs]
  }, [store, didStoreComeFromProps, contextValue])

  const overriddenContextValue = useMemo(() = > {
    if (didStoreComeFromProps) {
      return contextValue
    }

    return {
      ...contextValue,
      subscription,
    }
  }, [didStoreComeFromProps, contextValue, subscription])

  const [
    [previousStateUpdateResult],
    forceComponentUpdateDispatch,
  ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

  if (previousStateUpdateResult && previousStateUpdateResult.error) {
    throw previousStateUpdateResult.error
  }

  const lastChildProps = useRef()
  const lastWrapperProps = useRef(wrapperProps)
  const childPropsFromStoreUpdate = useRef()
  const renderIsScheduled = useRef(false)

  const actualChildProps = usePureOnlyMemo(() = > {
    if (
      childPropsFromStoreUpdate.current &&
      wrapperProps === lastWrapperProps.current
    ) {
      return childPropsFromStoreUpdate.current
    }

    return childPropsSelector(store.getState(), wrapperProps)
  }, [store, previousStateUpdateResult, wrapperProps])

  useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
    lastWrapperProps,
    lastChildProps,
    renderIsScheduled,
    wrapperProps,
    actualChildProps,
    childPropsFromStoreUpdate,
    notifyNestedSubs,
  ])

  useIsomorphicLayoutEffectWithArgs(
    subscribeUpdates,
    [
      shouldHandleStateChanges,
      store,
      subscription,
      childPropsSelector,
      lastWrapperProps,
      lastChildProps,
      renderIsScheduled,
      childPropsFromStoreUpdate,
      notifyNestedSubs,
      forceComponentUpdateDispatch,
    ],
    [store, subscription, childPropsSelector]
  )

  const renderedWrappedComponent = useMemo(
    () = > (
      <WrappedComponent
        {. actualChildProps}
        ref={reactReduxForwardedRef}
      />
    ),
    [reactReduxForwardedRef, WrappedComponent, actualChildProps]
  )

  const renderedChild = useMemo(() = > {
    if (shouldHandleStateChanges) {
      return (
        <ContextToUse.Provider value={overriddenContextValue}>
          {renderedWrappedComponent}
        </ContextToUse.Provider>)}return renderedWrappedComponent
  }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

  return renderedChild
}
Copy the code

Connectadvanced. js core code is ConnectFunction. Notice here that the childPropsSelector variable uses useMemo to generate the latest props whenever the store changes. Because childPropsSelector calls selectorFactory, the final result is structured as: (nextState, nextOwnProps) => nextFinalProps. The actualChildProps variable is the final computed props(nextFinalProps). When the props changes, a new WrappedComponent is returned at the renderedWrappedComponent location to re-render the component.

Subscriptions

The Subscriptions function in React-Redux is to subscribe to the stores used by the components, and if the stores change, the components are told to update.

const nullListeners = { notify(){}}function createListenerCollection() {
  const batch = getBatch()
  let first = null
  let last = null

  return {
    clear() {
      first = null
      last = null
    },

    notify() {
      batch(() = > {
        let listener = first
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },

    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },

    subscribe(callback) {
      let isSubscribed = true

      let listener = (last = {
        callback,
        next: null.prev: last
      })

      if (listener.prev) {
        listener.prev.next = listener
      } else {
        first = listener
      }

      return function unsubscribe() {
        if(! isSubscribed || first ===null) return
        isSubscribed = false

        if (listener.next) {
          listener.next.prev = listener.prev
        } else {
          last = listener.prev
        }
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    }
  }
}

export default class Subscription {
  constructor(store, parentSub) {
    this.store = store
    this.parentSub = parentSub
    this.unsubscribe = null
    this.listeners = nullListeners

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)}addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }

  notifyNestedSubs() {
    this.listeners.notify()
  }

  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }

  isSubscribed() {
    return Boolean(this.unsubscribe)
  }

  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)

      this.listeners = createListenerCollection()
    }
  }

  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}
Copy the code

(1) createListenerCollection ({callback, next, prev}) is used to add subscription methods. Once the notify method is triggered, the callback is executed via next to the next listener. (2) Subscription class whose constructor takes store and parentSub; store is the store generated by redux. In the **trySubscribe** method, you can see the use of this.store.subscribe(this.handlechangeWrapper), which means that once the store is updated, the subscription will be executed. ParentSub is an instance of Subscription. React-redux uses Subscription to subscribe to store and register callback functions. Whenever the Store changes, a callback is performed to update the relevant business component.

The following code implements the component subscription Store data update, which is in connectAdvance.js.

const [subscription, notifyNestedSubs] = useMemo(() = > {
  const subscription = new Subscription(
    store,
    didStoreComeFromProps ? null : contextValue.subscription
  )

  const notifyNestedSubs = subscription.notifyNestedSubs.bind(
    subscription
  )

  return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
Copy the code

React, Redux, react-redux

Steal a picture here. Where did you forget it came from