Redux系列-深入理解React-Redux

1,086 阅读7分钟

一、 前言

1.1 React-Redux是什么

顾名思义,React-Redux就是专门为 React 框架打造的Redux写法,相比传统的 Redux 框架,它有更多易用的Api可以简化写法,提高性能。

我们在实际的 React 项目中使用的更多的也是 React-Redux,而不会直接使用 Redux。

1.2 为什么需要 React-Redux

Redux系列-深入理解Redux中,我们知道了 Redux 本质上就是一个通过监听 store 更新而重新渲染页面进而修改UI的框架,我们可以这样使用。

const Counter = ({ value, onIncrement, onDecrement }) => (
  <div>
  <h1>{value}</h1>
  <button onClick={onIncrement}>+</button>
  <button onClick={onDecrement}>-</button>
  </div>
);

const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT': return state + 1;
    case 'DECREMENT': return state - 1;
    default: return state;
  }
};

const store = createStore(reducer);

const render = () => {
  ReactDOM.render(
    <Counter
      value={store.getState()}
      onIncrement={() => store.dispatch({type: 'INCREMENT'})}
      onDecrement={() => store.dispatch({type: 'DECREMENT'})}
    />,
    document.getElementById('root')
  );
};

render();
31 store.subscribe(render);

上面代码块是一个使用 Redux 实现的一个计数器例子,在第31行我们注册了store的监听,每次收到 action 都会重新调用 render 函数,使用最新的state渲染整个页面,这样效率显然是很低下的。

React-Redux的出现就是为了解决这一问题。它是一个结合了 React 的特性而打造的专门适用于 React 的 Redux 框架。

二、原理探究

2.1 React-Redux是如何更新页面的

在 Redux 中,我们需要通过 subscribe 方法手动注册对 store 中 state 变更的监听,从而在回调方法中更新页面,但是在使用 React-Redux的时候我们并没有手动注册,那么页面是如何更新的呢?

秘密就在 useSelector 这个hooks方法中,但是在介绍 这个方法之前,我们有必要先看一下 Provider 组件。

2.1.1 Provider组件

2.1.1.1 Provider的使用

<Provider store={store}>
	<App/>
</Provider>

我们将 redux 中的 store 传入 Provider 组件的 store 属性中,并且将 Provider 组件包裹在 App 的最外层。用法看起来很像 React 中的Context,实际上其组件内部用的确实是 Context,关于Context的使用可以参考:Context

2.1.1.2 Provider 源码

Provider组件的源码是这个,我们将源码进行一些删减,并添加一些注释。

function Provider({ store, context, children }) {
  // 初始化 contextValue,返回了一个包含store和subscription的对象,其中store就是我们参数中传入的自定义store,subscription可以理解为注册监听的集合
  const contextValue = useMemo(() => {
    const subscription = createSubscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

  // 保存状态
  const previousState = useMemo(() => store.getState(), [store])

  // 可以简单理解为 useEffect,在Provider组件加载的时候调用
  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue
    // 初始化 subscription
    subscription.trySubscribe()
		// state不相等的话,调用subscription中所有注册的回调
    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext
	// 使用 Context,其中 value 是一个封装了 store 和 subscription的对象。
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

可以看到Provider最终返回了一个 Context.Provider 组件,这样我们就可以在Provider 的子组件中获取到其value,这个value是一个封装了store和注册回调的对象。

2.1.2 useSelector

2.1.2.1 useSelector的使用

const dayModel = useSelector((state) => state.userName)

我们在函数式组件中一般通过如上方式使用 selector,这样在state.userName 改变的时候,组件就会重新渲染。

2.1.2.2 useSelector 源码

为什么使用 useSelector 后,当 userName 发生变化后就会使组件重新渲染呢?是不是因为我们注册了一些监听之类的呢,接下来看一下useSelector源码

const refEquality = (a, b) => a === b

3 function useSelectorWithStoreAndSubscription(
  selector,
  equalityFn,
  store,
  contextSub
) {
9   const [, forceRender] = useReducer((s) => s + 1, 0)

11   const subscription = useMemo(
    () => createSubscription(store, contextSub),
    [store, contextSub]
  )

16   const latestSubscriptionCallbackError = useRef()
  const latestSelector = useRef()
  const latestStoreState = useRef()
19   const latestSelectedState = useRef()

  const storeState = store.getState()
  let selectedState

  try {
    if (
26       selector !== latestSelector.current ||
      storeState !== latestStoreState.current ||
      latestSubscriptionCallbackError.current
    ) {
30      const newSelectedState = selector(storeState)
      if (
32       latestSelectedState.current === undefined ||
33       !equalityFn(newSelectedState, latestSelectedState.current)
      ) {
        selectedState = newSelectedState
      } else {
        selectedState = latestSelectedState.current
      }
    } else {
      selectedState = latestSelectedState.current
    }
  } catch (err) {
    if (latestSubscriptionCallbackError.current) {
      err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
    }
    throw err
  }

49  useIsomorphicLayoutEffect(() => {
    latestSelector.current = selector
    latestStoreState.current = storeState
    latestSelectedState.current = selectedState
    latestSubscriptionCallbackError.current = undefined
  })

56  useIsomorphicLayoutEffect(() => {
    function checkForUpdates() {
      try {
        const newStoreState = store.getState()
        if (newStoreState === latestStoreState.current) {
          return
        }

        const newSelectedState = latestSelector.current(newStoreState)

        if (equalityFn(newSelectedState, latestSelectedState.current)) {
          return
        }

        latestSelectedState.current = newSelectedState
        latestStoreState.current = newStoreState
      } catch (err) {
        latestSubscriptionCallbackError.current = err
      }

      forceRender()
    }

79    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    checkForUpdates()

    return () => subscription.tryUnsubscribe()
  }, [store, subscription])

  return selectedState
}

90 export function createSelectorHook(context = ReactReduxContext) {
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () => useContext(context)
  return function useSelector(selector, equalityFn = refEquality) {
96     const { store, subscription: contextSub } = useReduxContext()

98    const selectedState = useSelectorWithStoreAndSubscription(
      selector,
      equalityFn,
      store,
      contextSub
    )

    return selectedState
  }
}

109 export const useSelector = /*#__PURE__*/ createSelectorHook()

代码很多,就不一行行加注释了,抽几个重点的讲一下。

首先,在109行我们可以看到useSelector 实际上是调用了createSelector 函数,该函数会返回真正的useSelector方法

从第90行 createSelectorHook 的定义可以看到,其有一个参数 context,默认值是 ReactReduxContext,此Context就是 最外层的Provider组件所使用的Context,我们可以通过 createSelectorHook 来控制使用的 context。

在第96行中,我们从context中读取了我们在最外层的 Provider 组件中传入的 value,得到了store,和 subscription,并且把 subscription 重命名为 contextSub

在第98行中,我们调用 useSelectorWithStoreAndSubscription 方法并传入四个参数

  • selector 就是我们使用 useSelector 方法时传入的 selector 函数
  • equalityFn 该函数的定义在第 1 行,定义了selector返回值的比较方式,可以看到默认是浅比较。
  • store 就是我们在Provider中给Context传入的store
  • contextSub 就是我们context中的 subscription

useSelectorWithStoreAndSubscription 方法返回了一个state,并且在第105行返回,selectedState 就是我们调用selector方法后返回的数据,那么我们有理由推测 useSelectorWithStoreAndSubscription 的方法的作用就是调用selector方法返回数据,并且注册监听。

在第3行中定义了 useSelectorWithStoreAndSubscription 函数,

在第9行中,使用 useDispatch 方法获取了 forceRender 函数,关于 useReducer 可参考文档:useReducer,

由useReducer文档可知,forceRender函数仅仅是一个普通的 dispatch 函数,但是我们调用 dispatch 函数后效果和setState类似,会使当前组件重新render,所以其称为 forceRender函数也没什么问题。

第11行,我们可以简单理解为,通过context中的store和 subscription 生成一个新的 subscription

第16,19行定义了一堆 ref,接着在第26行判断selector是否有变化,state是否有变化等等,首次Ref为空肯定不等,所以进入第30行

在第30行中,我们终于调用了selector方法,传入 state,得到了我们想要的数据

在第32,33行进行 selector 返回值的判断,如果和旧值一样,就还用旧值,否则用新值

第49行是对ref的赋值

第56行的这个 useIsomorphicLayoutEffect 比较重要,其可以简单理解为 useEffect,重点在第79行和80行。

79行中,我们将checkForUpdates函数赋值给了 subscription的onStateChange 方法,并且在第80行调用了subscription.trySubscribe,进去trySubscribe方法实现看一下

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

      listeners = createListenerCollection()
    }
  }

可以看到其向store通过subscribe注册了回调,每次dispatch action的时候就会调用 handleChangeWrapper 而 handleChangeWrapper 函数的实现如下方所示

  function handleChangeWrapper() {
    if (subscription.onStateChange) {
      subscription.onStateChange()
    }
  }

调用了subscription的onStateChange,我们已经知道 useSelectorWithStoreAndSubscription 的 onStateChange 方法被赋值为了 checkForUpdates,

所以,store每次dispatch action都会调用 checkForUpdates函数。

我们回到第57行看一下 checkForUpdates 函数的实现,可以看到其通过 selector 算出值,并且调用 equalityFn 函数进行比较,如果不相等,最终会调用 forceRender,使当前界面重新render,使用新的selectedState重新渲染界面。

总结

useSelector 的作用是通过 selector 函数得到需要的数据,并且向 store 通过 subscribe 注册回调,在每次state发生变化的时候都会重新调用 selector 函数对其返回值使用比较函数进行比较,如果两者不同,就会重新渲染当前组件。

2.1.3 useDispatch 方法

相比 useSelector, useDispatch就简单多了,直接贴出源码

export function createDispatchHook(context = ReactReduxContext) {
  const useStore =
    context === ReactReduxContext ? useDefaultStore : createStoreHook(context)

  return function useDispatch() {
    const store = useStore()
    return store.dispatch
  }
}
export const useDispatch = /*#__PURE__*/ createDispatchHook()

可以看到就是直接从context中取到 store,然后return store的 dispatch方法。

2.1.4 connect 方法

useSelector 方法可以在函数式组件中使用,但如果我们使用的是类组件的话就无法使用Hooks了,这时候可以使用 connect。

2.1.3.1 connect的使用

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

嗯,使用上不过多解释了,大家可以参考这篇文章

2.1.3.2 connect 源码

源码地址是这个

connect相关联的代码很多,无法逐一深入探讨,我们抽几个核心的地方来讲一下。

首先,我们知道connect()返回一个高阶组件,关于高阶组件是什么可以参考这篇文章:高阶组件

我们需要重点关注的无非这两点:

  1. 我们是如何通过 mapStateToProps 和 mapDispatchToProps 分别把 state的数据和dispatch弄进组件的props的
  2. state更新的时候,我们的组件是如何更新的

关于第1点,我们需要看这段代码,需要注意其ConnectFunction函数

function ConnectFunction(props) {
      const [propsContext, reactReduxForwardedRef, wrapperProps] =
        useMemo(() => {
4          const { reactReduxForwardedRef, ...wrapperProps } = props
          return [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

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

      const [subscription, notifyNestedSubs] = useMemo(() => {
        if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
        const subscription = createSubscription(
          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)

 76 const actualChildProps = usePureOnlyMemo(() => {
        if (
          childPropsFromStoreUpdate.current &&
          wrapperProps === lastWrapperProps.current
        ) {
          return childPropsFromStoreUpdate.current
        }
 83       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
116       {...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
    }

通过代码我们可以知道,

这是一个函数式组件,这就意味着,其可以使用Hooks了!这很重要,因为我们之前分析过的 useSelector 里就大量使用了Hooks。

在上面代码块的第116行,我们给返回的组件添加了 props,所以接下来我们来看一下 actualChildProps 是怎么来的。

actualChildProps 的赋值在第 76 行,其通过一个 Hooks 函数赋值,可以简单将其理解为 useMemo,

可以看到在76行memo中,如果 props没变化的话,用之前的值,有变化就重新计算,我们要看有变化的情况,所以需要看第83行

83行调用 childPropsSelector 并传入当前state和 wrapperProps,在看函数的定义之前,我们有必要看一下 wrapperProps 是什么。

wrapperProps需要看第4行,可以发现其实对高阶组件 props 的解构,所以我们可以将 wrapperProps 理解为用户在使用组件的时候手动写的普通 Props,接下来我们可以安心去看 childPropsSelector 函数了

可以看第37行,函数式通过 createChildSelector生成的

    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      displayName,
      wrappedComponentName,
      WrappedComponent,
    }

    const { pure } = connectOptions

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

如上方代码第15行所示,跟 createChildSelector 函数,又跟到了 selectorFactory 函数,selectorFactory 接受 store.dispatch, selectorFactoryOptions作为参数,dispatch我们很熟悉,这个 selectorFactoryOptions我们可以将其简单理解为一堆配置信息,重点是 selectorFactory 是什么。

selectorFactory 是 connectAdvanced 的第一个参数,是从外面传进来的,通过查看调用逻辑,发现selectorFactory实际上是这个

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
  )
}

可以看到其根据 options 调用 pureFinalPropsSelectorFactory 或 impureFinalPropsSelectorFactory 将 mapStateToProps 和 mapDispatchToProps 组合并返回一个新对象,至于组合的细节因篇幅有限,不做过多了解,感兴趣的同学可自行看源码了解。

现在我们知道了第一个问题的答案,我们就是通过将组件的props和 mapStateToProps 返回的 props 和 mapDispatchToProps 返回的 props组合在一起,传到了子组件的props。


现在开始分析第二个问题,state 更新的时候,我们的组件是如何更新的。

首先思考,state更新的时候,我们组件会更新,那理论上需要向 store 通过 subscribe 注册了监听,重点是这段代码

useIsomorphicLayoutEffectWithArgs(
        subscribeUpdates,
        [
          shouldHandleStateChanges,
          store,
          subscription,
          childPropsSelector,
          lastWrapperProps,
          lastChildProps,
          renderIsScheduled,
          childPropsFromStoreUpdate,
          notifyNestedSubs,
          forceComponentUpdateDispatch,
        ],
        [store, subscription, childPropsSelector]
      )
//-----------
function useIsomorphicLayoutEffectWithArgs(
  effectFunc,
  effectArgs,
  dependencies
) {
  useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies)
}

这段代码是在函数式组件 ConnectFunction 内部调用的,可以简单理解为在 useEffect 方法中调用了 subscribeUpdates 函数,接下来我们看一下subscribeUpdates函数的实现

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 {
25      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } catch (e) {
      error = e
      lastThrownError = e
    }

    if (!error) {
      lastThrownError = null
    }

38    if (newChildProps === lastChildProps.current) {
      if (!renderIsScheduled.current) {
        notifyNestedSubs()
      }
    } else {
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true
      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED',
        payload: {
          error,
        },
      })
    }
  }
54  subscription.onStateChange = checkForUpdates
  subscription.trySubscribe()
  checkForUpdates()

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

    if (lastThrownError) {
      throw lastThrownError
    }
  }

  return unsubscribeWrapper
}

上面代码块和我们在 useSelector中看到的类似,重点需要关注的是参数中的 store 和 subscription。

我们在第54 行将 subscription.onStateChange 方法指定为 checkForUpdates 函数,并通过 subscription.trySubscribe() 向store注册监听(关于此方法是如何注册监听的,可参考 useSelector 那节)

这样,在每次dispatch action的时候,都会调用 checkForUpdates方法,在checkForUpdates方法中,我们在第25行算出新的 newChildProps ,在 38 行与旧的 props进行比较,可以看到这里只能进行浅比较,无法自定义比较函数。如果不相等,就调用 forceComponentUpdateDispatch 发送 STORE_UPDATED Action,和 useSelector 中的 forceRender 类似,这个 forceComponentUpdateDispatch 是在如下代码块中赋值的

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

可以看到其也是通过 useReducer 返回的一个dispatch,调用 dispatch 会使当前组件重新渲染,从而实现了使用最新的props更新页面的效果。

总结:

由于类组件中无法使用 Hooks,所以我们通过高阶组件,在组件外面又包了个函数式组件,并在函数式组件中注册对 store 的监听,这样当 store 发生变化的时候,我们会再次生成新的 Props,通过和上一次生成的 props 做比较来决定是否要更新当前函数式组件。

总结

React-Redux通过借助 React 中特有的Context,Hooks,高阶组件等,使得 Redux 在 React 中的使用更加方便高效。