React实现控制减少useContext导致非必要的渲染详解
CBDxin 人气:0前言
在我们使用useContext来进行数据流管理时,每当context更新时,所有使用到该context的组件都会重新渲染。如果我们的context的数据是由多个部分组成的,但只有其中一两个字段会频繁更新,但其他的数据都比较稳定时,这时,即使组件值使用到了比较稳定的那部分数据,但它依然会频繁渲染,这就很容易会导致性能问题。我们一般会使用拆分context或者结合useMemo来减少组件渲染的次数:
1.拆分context
我们可以通过将context拆分为承载不稳定数据的instableContext和承载稳定数据的stableContext。
const InstableStateContext = React.createContext(); const StableStateContext = React.createContext(); function Provider({children}) { const [instableState, instableDispatch] = React.useState(); const [stableState, stableDispatch] = React.useState(); return ( <StableStateContext.Provider value={{state:stableState, dispatch:stableDispatch}}> <InstableStateContext.Provider value={{state:instableState, dispatch:instableDispatch}}> {children} </InstableStateContext.Provider> </StableStateContext.Provider> ) }
在只使用稳定数据的组件中,我们只去使用stableContext,
//stableComponent.js function stableComponent() { const {state} = React.useContext(StableStateContext); return ...; }
这能够让stableComponent.js只有在StableStateContext中的数据更新时,才会触发渲染,而不需要关心InstableStateContext
2.使用useMemo包裹函数
useMemo可以传入一个数据生成函数和依赖项,它可以使数据生成函数当且仅当依赖性发生变化时,才会重新计算要生成的数据的值。我们可以将组件的返回值使用useMemo进行包裹,把要使用的数据作为依赖项传入
const {state}= useContext(AppContext); return useMemo(() => <span>data:{state.depData}</span>, [state.depData]);
在上面的例子中,当且仅当depData发生变化时,该组件才会重新渲染。
虽然上面两种方法都可以减少一些不必要的渲染,但写起来总觉得不够优雅(很麻烦)。下面我们来讲讲另一种减少使用useContext导致的不必要渲染的方法。
使用发布订阅减少使用useContext导致的不必要渲染
我们有没有办法做到只有在我们使用到的context数据发生变化时,才去触发渲染,而不需要使用useMemo进行繁琐的包裹呢。
我们可以创建这么一个store,它拥有一个getState方法可以用来获取context中存储的数据。
const [state, dispatch] = useReducer(this.reducer, initState); const store = { getState: () => state, dispatch, }
我们使用useMemo对store的值进行包裹,且deps为空数组:
const [state, dispatch] = useReducer(this.reducer, initState); const store =useMemo(() => ({ getState: () => state, dispatch, }),[]);
这样store的值的引用便不会发生改变,如果把store作为context.Provider的value值进行传递:
Provider = (props: ProviderProps) => { const { children, initState = {} } = props; const [state, dispatch] = useReducer(this.reducer, initState); //store值不会更新,所以不会触发渲染 const store = useMemo( () => ({ getState: () => cloneDeep(state), dispatch, }), [], ); return <this.context.Provider value={store}>{children}</this.context.Provider>; };
这样Provider下的组件便不会因为state的变化而触发渲染。但这样的话,因为store的值没有发生变化,provider内的组件便没有办法得知该何时去渲染了。这时我们引入发布订阅模式,来通知组件该何时渲染。当state发生变化时,我们会触发stageChange
事件:
Provider = (props: ProviderProps) => { const { children, initState = {} } = props; const [state, dispatch] = useReducer(this.reducer, initState); useEffect(() => { //告知useSelector,state已更新,让它触发forceUpdate this.emit('stateChange'); }, [state]); //store值不会更新,所以不会触发渲染 const store = useMemo( () => ({ getState: () => cloneDeep(state), dispatch, }), [], ); return <this.context.Provider value={store}>{children}</this.context.Provider>;
在下面讲到的useSelector中会订阅此事件来告知组件需要重新渲染了。
接下来我们会实现一个useSelector方法,作为我们在组件内获取state中的数据的桥梁,他接收一个selector函数作为参数,如:
const a = useSelector(state=>state.a)
这样,我们就可以获取到state中的a。接下来我们要做的就是如何使得当state.a更新时,组件能够触发渲染,同时获取到最新的a。
上面说到,在useSelector中,我们会订阅stageChange事件,这时,我们会检查selector选中的数据有没有发生变化,有的话便使用forceUpdate
进行强制渲染;
useSelector: UseSelector = (selector) => { const forceUpdate = useForceUpdate(); const store = useContext<any>(this.context); const latestSelector = useRef(selector); const latestSelectedState = useRef(selector(store.getState())); if (!store) { throw new Error('必须在Provider内使用useSelector'); } latestSelector.current = selector; latestSelectedState.current = selector(store.getState()); useEffect(() => { const checkForUpdates = () => { const newSelectedState = latestSelector.current(store.getState()); //state发生变化时,检查当前selectedState和更新后的SelectedState是否一致,不一致则触发渲染 if (!isEqual(newSelectedState, latestSelectedState.current)) { forceUpdate(); } }; this.on('stateChange', checkForUpdates); return () => { this.off('stateChange', checkForUpdates); }; }, [store]); return latestSelectedState.current; };
forceUpdate
的原理也很简单,通过变更一个无用的状态来触发组件更新:
const useForceUpdate = () => { const [_, setState] = useState(false); return () => setState((val) => !val); };
就这样,当我们在组件时使用useSelector时获取数据时,只有在selector选中的数据被更新时,组件才会重新渲染。
加载全部内容