前言
类组件中可以使用 useSelector 吗?那必然是不能的,因为 useSelector 是 Hook 函数,而 Hook 函数只能在函数式组件中使用。
但是,当我们了解了useSelector原理后, 就可以自己实现一个具有类似 useSelector 函数功能,并且可以在类组件中使用的函数。
实现
首先我们知道 useSelector 的原理是:
通过 selector 函数得到需要的数据,并且向 store 通过 subscribe 注册回调,在每次 state 发生变化的时候都会重新调用 selector 函数对其返回值使用比较函数进行比较,如果两者不同,就会重新渲染当前组件。
所以目前我们需要解决两个问题:
- 如何获取 store
- 如何更新组件
如何获取 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 代替上面写法。