React-Redux7源码解析——Provider

634 阅读2分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

React Redux8已经使用了React18的useSyncExternalStore,不过还未完成,代码中还。因此本系列将从React Redux 7开始,等8正式出来后再进行比较。关于uSES的用法可以参考前文juejin.cn/post/705658…

React Redux中核心api也不多,关键的两个是Providerconnect。本系列从Provider开始,看看React Redux到底是如何运行的。

React Redux是如何触发更新的

分析源码必须带着问题,React Redux解决的核心问题就是触发react组件的更新。

先来一段简单的代码

import { createStore } from 'redux';
import { Provider, connect } from '../react-redux';
//创建store
const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD':
      state.list.push(action.payload)
      state.list = [...state.list]
      return { ...state }
    case 'CLEAR':
      return { ...state, list: [] }
    case 'DELETE':
      state.list.splice(action.payload, 1);
      return { ...state }
    default:
      return state
  }
}

const store = createStore(reducer, { list: ['read'] })


// 外层组件
export default function IndexPage() {
  return (
    <Provider store={store}>
      <TestWrap name={'TODO LIST'}></TestWrap>
    </Provider>
  );
}

function Test(props) {
  return <div>
    <div>name:{props.name}</div>
    <div><ul>{props.list.map(item => <li key={item}>{item}</li>)}</ul></div>
    <button onClick={() => props.add('eat')}>+</button>
    <button onClick={() => props.clear()}>clear</button>
  </div>
}
function mapStateToProps(state, props) {
  return { list: state.list, ...props }
}
function mapDispatchToProps(dispatch) {
  return {
    add(item) {
      dispatch({ type: 'ADD', payload: item })
    },
    clear() {
      dispatch({ type: 'CLEAR' })
    }
  }
}
const TestWrap = connect(mapStateToProps, mapDispatchToProps)(Test)

image.png

这段代码的效果是:点击*+按钮会增加一条todo,点击clear*会清空。

Test组件中调用的dispatch,就是redux store的dispatch方法,这个方法会触发store的更新,但并不能导致组件更新。React Redux中将组件与store进行衔接的api,首当其冲的就是Provider

Provider

大家都知道React Redux使用了contextapi,而Provider组件就是react context与 redux store的一个衔接点。

用法

Provider可以传入一个context对象和一个redux store。大多数时候我们都不必指定context,因为它内部会自己创建一个。

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

核心源码

这里将源码稍微做了一些改动,大致如下:

import { createSubscription } from '../utils/Subscription'
const ReactReduxContext = React.createContext(null)
function Provider({ store, context, children }) {
  const contextValue = useMemo(() => {
    const subscription = createSubscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

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

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

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

  const Context = ReactReduxContext
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

export default Provider

ReactReduxContext

React Redux内部创建的context便是ReactReduxContext,并且这个context也被导出了,代码中可以直接使用

import { ReactReduxContext } from 'react-redux'

function MyConnectedComponent() {

  const { store } = useContext(ReactReduxContext)

  return (
    <ReactReduxContext.Consumer>
      {({ store }) => { }}
    </ReactReduxContext.Consumer>
  )
}

contextValue

contextValue是一个对象,结构如下:

{
  store,
  subscription:{
    addNestedSub,
    notifyNestedSubs,
    handleChangeWrapper,
    isSubscribed,
    trySubscribe,
    tryUnsubscribe,
    getListeners: () => listeners,
  }
}

这个对象简而言之,是在store之外增加了一层自己的订阅。

Provider的代码比较简单,它主要做了这几件事情:

  • 创建一个context
  • 创建一个subscription
  • 将redux store与subscription设置为context的value
  • 订阅store,当store发生变化时执行subscription.onStateChange

需要注意的是,当前subscriptiononStateChange尚未设置。因此仅有Provider,是无法更新组件的。

直到有组件调用connect,context中的subscription才真正有了自己的价值。

未完待续

总结下来,Provider的源码对我们理解React Redux核心原理的价值不大,真正的核心实现在connect中。

但是,它的源码埋了一个伏笔——React Redux为何要费尽再搞一个subscription进行订阅?