之前研究 ant design
的时候发现,里面提到了可以使用 react-context-selector
优化 context
。下面来研究一下这个三方库的原理。
先说问题,如果 provider
向底层传递的是一个对象 { a, b }
,A
组件依赖 a
属性,B
组件依赖 b
属性,当 a/b
任意一个属性改变的时候,组件 A
或 B
都会 rerender
。
想要避免这个 rerender
,可以将 a/b
两个属性拆成两个 provider
,这种方式的缺点就是当你有很多个属性的时候,要拆成多个 provider
,复杂度会增加特别多。(当然组件本身的 memo
肯定不能少)。
另一种方式就是使用订阅-发布的方式,核心的思想就是,组件订阅消息内部 forceUpdate,provider 发布消息。
const MyCONTEXT = createContext({} as any)
const listeners = new Set()
const MyProvider = ({ value, children }: any) => {
const valueRef = useRef(value)
useLayoutEffect(() => {
valueRef.current = value
// 向所有子组件发送消息
listeners.forEach((listener: any) => {
listener([valueRef.current, value])
})
}, [value])
// 这里的 value 永远都不会改变,所以子组件不会更新
// 不能传 ref.current
return <MyCONTEXT.Provider value={valueRef}>{children}</MyCONTEXT.Provider>
}
const useMyContext = (selector: any) => {
const { current: value } = useContext(MyCONTEXT)
const selected = selector(value)
const [, dispatch] = useReducer(
(pre: any, next: any) => {
// 判断前后两次是否相等,相等的话返回原对象,跳过更新
if (pre.selected === selector(next[1])) {
return pre
}
return { value: next[1], selected: selector(next[1]) }
},
{ value, selected }
)
useLayoutEffect(() => {
// 添加监听者
listeners.add(dispatch)
return () => {
listeners.delete(dispatch)
}
}, [listeners])
return selected
}
let renderCount = 0
const ContextDemo = () => {
const [state, setState] = useState(0)
const [value, setValue] = useState({ count: 0, text: 'text' })
const [, forceUpdate] = useReducer(
(pre: any, next: any) => {
return pre
},
{ name: 'xxx', age: 10 }
)
renderCount++
return (
<SafeAreaView>
<Button
title={`测试 reducer`}
onPress={() => {
forceUpdate({})
}}
/>
<Button title={`测试-${state}`} onPress={() => setState(state + 1)} />
<Button
title={`setValue - 改变了 count 及 text`}
onPress={() => {
setValue({ count: value.count + 1, text: value.text + 't' })
}}
/>
<Button
title={`setValue - 仅改变了 count`}
onPress={() => {
setValue({ ...value, count: value.count + 1 })
}}
/>
<Button
title={`setValue - 仅改变了 text`}
onPress={() => {
setValue({ ...value, text: value.text + 'o' })
}}
/>
<Text>render count: {renderCount}</Text>
<MyProvider value={value}>
<Child1 />
<Child2 />
</MyProvider>
<Text fontWeight="bold">
1. 如果改变的不是 value,子组件中使用 memo,是可以防止 rerender 的
</Text>
<Text fontWeight="bold">
2. Provider 的 value 改变了之后,子视图无论如何都会触发 rerender 的
</Text>
</SafeAreaView>
)
}
let child1RenderCount = 0
const Child1 = memo(() => {
// const value = useMyContext()
const count = useMyContext((value: any) => value.count)
child1RenderCount++
return (
<>
<Text>child1 - {count}</Text>
<Text>child1RenderCount - {child1RenderCount}</Text>
</>
)
})
let child2RenderCount = 0
const Child2 = memo(() => {
const text = useMyContext((value: any) => value.text)
child2RenderCount++
return (
<>
<Text>child2 - {text}</Text>
<Text>child2RenderCount - {child2RenderCount}</Text>
</>
)
})
很多状态管理库都有这样的思想,redux
、mobx
感觉都有。