react-redux原理浅析

1,982 阅读5分钟

一、 事件处理,注册-监听模式

前端的注册 - 监听模式想必大家都很熟悉。预先注册某一个特殊事件的回调,等待事件发生时调用回调函数进行响应。

最常见的原生注册 - 监听模式就是dom上的事件响应函数了。例如点击事件、拖拽事件、鼠标滚动事件等等。

若在一个系统中,需要感应某一些变量的变化。变量变化后,需要触发一些其他操作,该如何实现呢?

思路一: 在每次变量变化处,广播一个自定义事件。在需要监听的地方调用响应函数。由于变量的变化可以发生在代码的任意地方,则追踪触发源比较困难,不利于整体的维护。

思路二: 在一个地方集中感知变量的变化 -> 预先注册(储存)回调函数,变化之后手动调用回调函数。

思路三: 对每个对象设置代理。在set方法成功后,触发回调函数。

在我的理解中,redux更像思路二的实现。

二、redux中的store是什么

redux实现了store对象的创建。暴露了获取需要追踪对象的方法,改变对象的方法,以及注册对象改变时候的监听函数接口。如下列表所示:

  • getState() : 获取当前store的最新state变量。
  • dispatch(dispatchParams) : 修改store的state变量,并且触发监听函数。通过传入参数的dispatchParams.action来调用对应的reducer, 在对应的reducer中返回新的state,更新store中的state变量,调用监听函数。
  • subscribe(listener): 注册监听函数。当store.dispatch()函数被调用后,触发此监听函数。可以多次调用subscribe来注册多个监听函数。监听函数存储在一个数组中,被依次调用。

store这个变量通过redux提供的createStore方法创建。

二、react-redux是如何扩展redux的?

react-redux库最重要的是提供了三个主要的api: Provider、useDispatch以及useSelector。

这三个api的作用分别是:

  • Provider:是一个react组件,提供将一个redux的store变量“注入”到当前app中的能力。
  • useDispatch: 提供变更store中state变量的能力,底层其实就是调用了store.dispatch方法。
  • useSelector: 提供返回store中state某个变量值,并监听该值的改变,触发视图更新。这个功能是对redux库中getState()功能的扩展:不仅返回最新state的值,并且可以触发调用useSelector()组件的视图更新。这恰恰是react-redux中的核心能力。

Provider

Provider组件其实是借助react的context来实现的。它只是一个注入context的根组件。如下代码所示。

interface ReactReduxContextValue {
  store: // redux store
  subscription: // react-redux subscription
}
const ReactReduxContext = React.createContext<ReactReduxContextValue | null>(null)
const Provider = (props) => {
    const subscription = createSubscription(store); //这个createSubscription方法是react-redux提供的。
    const contextValue = {
        store: props.store, 
        subscription: subscription
    }

    return (<ReactReduxContext.Provider value={contextValue} >
                {props.children}
            </ReactReduxContext.Provider>
            )

}

这里的contextValue只有两个属性:store和subscription。一般情况下引起的更新只是contextValue.store中state的更新,而不是这个contextValue的更新。这样保证了顶层组件的稳定性:不会由于某个操作而触发整个app的更新。

一般的使用场景是在应用中,通过redux提供的createStore方法生成一个store对象,作为Provider组件的value props传入。在react-redux内部,这个store会赋值给contextValue中的store。后续在整个程序代码中,都可以很方便的获得contextValue.store,进而能获取store上提供的能力。例如获取state的值,修改state的值,注册监听函数等。

contextValue中subscription变量,是react-redux提供的一个注册方法。之前说过,store中提供的dispatch函数,实现了更新state且调用回调函数。subscription对象中的trySubscribe方法调用了store.subscribe(handleFunction),保证在调用store.dispatch时,handleFunction会被调用。只不过这个handleFunction不是应用app传入的,而是subscription对象的内部方法。该方法会在某个特定条件下调用应用app传入的另外一个回调。这个机制是实现react组件细粒度监听state中某一变量变化的基础。关于这个过程,在useSelector部分会有详细说明。

useDispatch

在dispatch部分,reaxt-redux并没有做额外的工作,只是返回了store.dispatch方法供调用。

useSelector

这个组件正是react-redux巧妙的地方。

首先,这个函数的入参是一个函数fn。 fn的签名如下: state => state.certainValue。

使用方式:

const data = useSelector(state => state.a);

那么,它是如何实现调用上述一行代码,就可以监听变量的变化,且触发视图更新的呢?

还记得Provider注入的contextValue吗?其中包含了store和subscription。获取变量容易,直接在store中获取当前的变量:

const state = store.getState();
const value = fn(state); //这里的fn是useSelector的入参: state => state.a;
return falue;

触发组件更新,也较容易想到。借助react提供的useState或者useReducer hook来触发组件更新。 前文中说过,变量变化是调用useDispatch而触发的。那在useDispatch后,发觉前一次的state.a和本次的state.a不一样,就可以调用useState/useReducer提供的更新函数,就能达到更新视图的目的。用代码实现如下:

import { useReducer } from react; 

const [, forceRender] = useReducer(s => s + 1, 0);

//此处的forceRender函数,就是触发视图更新的函数。

何时调用forceRender呢?前面已经讲过,在useSelector内部,可以获取到state.a的值。那么监听state的变化,且比对变化前后state.a的值,在state.a改变之后,再调用forceRender, 不就实现了某个组件监听state.a且更新视图的目的了吗?代码如下:

const {contextValue } = useContext(ReactReduxContext);
const {store } = contextValue;
const oldValue = useRef(undefined);
useIsomorphicLayoutEffect({
    const checkForUpdate = (){
        const newState = store.getState();
        const newValue = fn(newState);
        if(oldValue.current === newValue){
            return ;  //数据无变化 
        }
        if(oldValue.current !== newValue){
            forceRender();
        }
    }
    store.subscribe(checkForUpdate); (1) //注册之后,在程序中调用useDispatch后,会调用getValueAndUpdateView函数。但是若比较到state.a没有变化时,也不会调用forceRender。只有当state.a变化时,才会forceRender。

}, [store, subscription])


至此,完成了useSelector的主要功能的实现。

在上述代码中,(1)处: store.subscribe(checkForUpdate)只是举例作用。在实际实现中,react-redux并不是直接调用store.subscribe, 而是通过subscription对象实现的。

subscription上有注册方法:trySubscribe, 写法如下:

 function trySubscribe() {
    if (!unsubscribe) {
      unsubscribe = store.subscribe(handleChangeWrapper)

      listeners = createListenerCollection()
    }
  }
  
 function handleChangeWrapper(){
    if (subscription.onStateChange) {  //这里的subscription就是subscription自己。onStateChange是后续赋值的。
      subscription.onStateChange()
    }
 }

在useSelector中,实现了这个onStateChange函数:

useIsomorphicLayoutEffect(() => {
 function checkForUpdate(){
  ...
 }
 subscription.onStateChange = checkForUpdate;   //给subscription.onStateChange赋值。
 subscription.trySubscribe();

}, [])

至此,useSelector就实现了单个变量的变化监听。