优化 React context

14 阅读2分钟

之前研究 ant design 的时候发现,里面提到了可以使用 react-context-selector 优化 context。下面来研究一下这个三方库的原理。

先说问题,如果 provider 向底层传递的是一个对象 { a, b }A 组件依赖 a 属性,B 组件依赖 b 属性,当 a/b 任意一个属性改变的时候,组件 AB 都会 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>
    </>
  )
})

很多状态管理库都有这样的思想,reduxmobx 感觉都有。