要强制更新当前组件的方法不止一个,如果你是用的Class组件,你可以直接this.setState({}),老版的React-Redux就是这么干的。但是新版React-Redux用hook重写了,那我们可以用React提供的useReducer或者useStatehook,React-Redux源码用了useReducer,为了跟他保持一致,我也使用useReducer:
function storeStateUpdatesReducer(count) { return count + 1; } // ConnectFunction里面 function ConnectFunction(props) { // ... 前面省略n行代码 ... // 使用useReducer触发强制更新 const [ , forceComponentUpdateDispatch ] = useReducer(storeStateUpdatesReducer, 0); // 注册回调 store.subscribe(() => { const newChildProps = childPropsSelector(store, wrapperProps); if(!shallowEqual(newChildProps, lastChildProps.current)) { lastChildProps.current = newChildProps; forceComponentUpdateDispatch(); } }); // ... 后面省略n行代码 ... }connect这块代码主要对应的是源码中connectAdvanced这个类,基本原理和结构跟我们这个都是一样的,只是他写的更灵活,支持用户传入自定义的childPropsSelector和合并stateProps, dispatchProps, wrapperProps的方法。有兴趣的朋友可以去看看他的源码:https://github.com/reduxjs/react-redux/blob/master/src/components/connectAdvanced.js
到这里其实已经可以用我们自己的React-Redux替换官方的了,计数器的功能也是支持了。但是下面还想讲一下React-Redux是怎么保证组件的更新顺序的,因为源码中很多代码都是在处理这个。
保证组件更新顺序前面我们的Counter组件使用connect连接了redux store,假如他下面还有个子组件也连接到了redux store,我们就要考虑他们的回调的执行顺序的问题了。我们知道React是单向数据流的,参数都是由父组件传给子组件的,现在引入了Redux,即使父组件和子组件都引用了同一个变量count,但是子组件完全可以不从父组件拿这个参数,而是直接从Redux拿,这样就打破了React本来的数据流向。在父->子这种单向数据流中,如果他们的一个公用变量变化了,肯定是父组件先更新,然后参数传给子组件再更新,但是在Redux里,数据变成了Redux -> 父,Redux -> 子,父与子完全可以根据Redux的数据进行独立更新,而不能完全保证父级先更新,子级再更新的流程。所以React-Redux花了不少功夫来手动保证这个更新顺序,React-Redux保证这个更新顺序的方案是在redux store外,再单独创建一个监听者类Subscription:
Subscription负责处理所有的state变化的回调
如果当前连接redux的组件是第一个连接redux的组件,也就是说他是连接redux的根组件,他的state回调直接注册到redux store;同时新建一个Subscription实例subscription通过context传递给子级。
如果当前连接redux的组件不是连接redux的根组件,也就是说他上面有组件已经注册到redux store了,那么他可以拿到上面通过context传下来的subscription,源码里面这个变量叫parentSub,那当前组件的更新回调就注册到parentSub上。同时再新建一个Subscription实例,替代context上的subscription,继续往下传,也就是说他的子组件的回调会注册到当前subscription上。
当state变化了,根组件注册到redux store上的回调会执行更新根组件,同时根组件需要手动执行子组件的回调,子组件回调执行会触发子组件更新,然后子组件再执行自己subscription上注册的回调,触发孙子组件更新,孙子组件再调用注册到自己subscription上的回调。。。这样就实现了从根组件开始,一层一层更新子组件的目的,保证了父->子这样的更新顺序。
Subscription类所以我们先新建一个Subscription类:
export default class Subscription { constructor(store, parentSub) { this.store = store this.parentSub = parentSub this.listeners = []; // 源码listeners是用链表实现的,我这里简单处理,直接数组了 this.handleChangeWrapper = this.handleChangeWrapper.bind(this) } // 子组件注册回调到Subscription上 addNestedSub(listener) { this.listeners.push(listener) } // 执行子组件的回调 notifyNestedSubs() { const length = this.listeners.length; for(let i = 0; i < length; i++) { const callback = this.listeners[i]; callback(); } } // 回调函数的包装 handleChangeWrapper() { if (this.onStateChange) { this.onStateChange() } } // 注册回调的函数 // 如果parentSub有值,就将回调注册到parentSub上 // 如果parentSub没值,那当前组件就是根组件,回调注册到redux store上 trySubscribe() { this.parentSub ? this.parentSub.addNestedSub(this.handleChangeWrapper) : this.store.subscribe(this.handleChangeWrapper) } }Subscription对应的源码看这里。
改造Provider