如何在类组件中使用 useSelector

1,700 阅读3分钟

前言

类组件中可以使用 useSelector 吗?那必然是不能的,因为 useSelector 是 Hook 函数,而 Hook 函数只能在函数式组件中使用。

但是,当我们了解了useSelector原理后, 就可以自己实现一个具有类似 useSelector 函数功能,并且可以在类组件中使用的函数。

实现

首先我们知道 useSelector 的原理是:

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

所以目前我们需要解决两个问题:

  1. 如何获取 store
  2. 如何更新组件

如何获取 store

我们知道 store 保存在了 ReactReduxContext 中,我们在类组件中也可以获取 Context,自然就也能获取 store。

首先自定义我们自己的 useSelector 函数,将其命名为 shiyongSelector

export function shiyongSelector(component, selector) {
    const { store, subscription: contextSub } = component.context
    const selectedState = selectorWithStoreAndSubscription(component, selector, equalityFn, store, contextSub)
    return selectedState
}

该函数比 useSelector 多了第一个参数,需要传入当前组件,我们解构出组件中context中的 store 和 subscription。

然后调用 selectorWithStoreAndSubscription 函数进行对 store 中state的注册监听以及state的计算,最终返回的 selectedState 就是我们通过 selector 计算出的需要的数据。

如何更新组件

selectorWithStoreAndSubscription 函数仅仅是对 useSelector 源码中的 useSelectorWithStoreAndSubscription 函数稍作修改,删除了Hooks相关代码。

function selectorWithStoreAndSubscription(
    component,
    selector,
    equalityFn,
    store,
    contextSub
) {
    let subscription;
    subscription = subscription ? subscription : createSubscription(store, contextSub);
    let haveInit
    let latestSelector
    let latestStoreState
    let latestSelectedState

    const storeState = store.getState()
    let selectedState
    try {
        if (
            selector !== latestSelector ||
            storeState !== latestStoreState
        ) {
            const newSelectedState = selector(storeState)
            if (
                latestSelectedState === undefined ||
                !equalityFn(newSelectedState, latestSelectedState)
            ) {
                selectedState = newSelectedState
            } else {
                selectedState = latestSelectedState
            }
        } else {
            selectedState = latestSelectedState
        }
    } catch (err) {
        throw err
    }
    if (!latestSelector) {
        latestSelector = selector
    }
    if (!latestStoreState) {
        latestStoreState = selector
    }
    if (!latestSelectedState) {
        latestSelectedState = selectedState
    }
    if (!haveInit) {
      	haveInit = true
        function checkForUpdates() {
            try {
                const newStoreState = store.getState()
                // Avoid calling selector multiple times if the store's state has not changed
                if (newStoreState === latestStoreState) {
                    return
                }

                const newSelectedState = latestSelector(newStoreState)

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

                latestSelectedState = newSelectedState
                latestStoreState = newStoreState
            } catch (err) {
                throw err
            }
67            component.forceUpdate()
        }

        subscription.onStateChange = checkForUpdates
        subscription.trySubscribe()

        checkForUpdates()
    }

    return selectedState
}

需要注意的是第67行,我们通过component.forceUpdate()来更新component, 而在 useSelectorWithStoreAndSubscription 源码中是通过Hooks函数来更新的。

额外

想要在类组件中使用 Context,我们还需要给当前类组件指定 contextType为ReactReduxContext

class TestClassComponent extends Component {
2    static contextType = ReactReduxContext
    constructor(props) {
        super(props);
    }
    render() {
7        this.text = shiyongSelector(this,(state) => state.weekSlice.weekGoal)
8        let dispatch = shiyongDispatch(this)
        console.log("TestClassComponent render:" + this.text)
        return <span onClick={() => {
            dispatch(WeekActions.updateGoal(this.text + 1))
        }
        }>{this.text}</span>
    }
}

注意第2行的 contextType = ReactReduxContext ,我们想要在类组件中使用 Context 少不了这一步,这也算是 shiyongSelector 函数的一个缺陷吧~

在第8行中我们使用自定义的 shiyongDispatch 来获取dispatch,shiyongDispatch 源码就相对简单多了。

export function shiyongDispatch(component) {
    const { store } = component.context
    return store.dispatch
}

整个组件的功能是:

每次点击按钮,就给按钮上显示的字符串末尾+1,经测试没有问题,可以正常更新UI。

注意:

此函数仅作为学习,未经充分测试,不建议在实际项目中使用。

建议使用 mapStateToProps 代替上面写法。