关于ReactContext你不知道的那些事

224 阅读3分钟

前言

  • createContextReact提供的一个全局的状态管理的API,可以不依赖父子传递从而做到状态共享
  • 为了组件之间的通信更加方便

如何使用

  • 官网描述
  • createContext注册之后会提供两个组件
  • Provider 属性是value,传入待消费的内容
  • Consumer 传入的是children,并且是一个函数
  • hooks之后是useContext,可以消费组件的状态
import {createContext, useState, useContext} from 'react'
// 执行createContext,一个Context
const Context = createContext<{name: number | undefined}>({name: undefined})

const Demo1 = () => {
  console.log('Demo1')
  const ctx = useContext(Context)
  return <div>{ctx.name}</div>
}

const Demo2 = () => {
  console.log('Demo2')
  return <Context.Consumer>
    {(state) => {
      return <div>{state.name}</div>
    }}
  </Context.Consumer>
}

const Demo3 = () => {
  console.log('Demo3')
  const ctx = useContext(Context)
  return <div>{ctx.name}</div>
}

const App = () => {
  const [state, setState] = useState({name:2})
  const handleClick = () => {
    setState({name: Math.random()})
  }
  const value = {...state}
  
  return <>
  		<button onClick={handleClick}>点击更改name</button>
      <Context.Provider value={value}>
        <Demo1 />
        <Demo2 />
        <Demo3 />
      </Context.Provider>
    </>
}

export default App

存在的问题

  • 刚刚那段代码执行之后,我们发现只要点击按钮,里面的每一个组件都会重新渲染这就会引发性能问题
  • 这个原因是因为React在创建组件的时候,props每一次都是一个新的对象

如何改造

import React, {createContext, useState, useContext} from 'react'
// 执行createContext,一个Context
const Context = createContext<{name: number | undefined}>({name: undefined})

const Demo1 = () => {
  console.log('Demo1')
  const ctx = useContext(Context)
  return <div>{ctx.name}</div>
}

const Demo2 = () => {
  console.log('Demo2')
  return <Context.Consumer>
    {(state) => {
      return <div>{state.name}</div>
    }}
  </Context.Consumer>
}

const Demo3 = () => {
  console.log('Demo3')
  return <div>Demo3</div>
}

const App = ({children}:{children:React.ReactNode}) => {
  const [state, setState] = useState({name:2})
  const handleClick = () => {
    setState({name: Math.random()})
  }
  const value = {...state}
  
  return <>
  		<button onClick={handleClick}>点击更改name</button>
      <Context.Provider value={value}>
        {children}
      </Context.Provider>
    </>
}


const Wrapper = () => {
  return <App>
    <Demo1 />
    <Demo2 />
    <Demo3 />
  </App>
}

export default Wrapper
  • 这里其实还有一个问题,那就是使用useContext的时候其实也会有重新渲染的问题
  • 使用Context.Consumer不会引发重新渲染

为什么渲染children不会引发重新渲染

  • 我们再来改造一下这个demo
  • Context.Provider其实还是一个组件,在Context.Providervalue发生变化,重新渲染的时候,它里面的组件都会重新渲染
  • 内部会转化成createElement('xxx',{}),想要不变化,直接使用children就可以了
let oldChildren
const App = ({children}:{children:React.ReactNode}) => {
  const [state, setState] = useState({name:2})
  const handleClick = () => {
    setState({name: Math.random()})
  }
  const value = {...state}
  // 这里做两个children的判断
  console.log('children是否相等', children === oldChildren)
  oldChildren = children
  
  return <>
  		<button onClick={handleClick}>点击更改name</button>
      <Context.Provider value={value}>
        {children}
      </Context.Provider>
    </>
}

解决useContext引发重新渲染的问题

  • 使用useMemo
const Demo1 = memo(() => {
  console.log('Demo1')
  const ctx = useContext(Context)

  const dom = useMemo(() => {
    return <div>{ctx.name}</div>
  }, [ctx.name])

  return dom
})

React如何实现的context

  • context的标签类型
  • REACT_CONTEXT_TYPE = symbolFor('react.context')判断REACT_CONTEXT_TYPE的时候返回 Context.Consumer
  • REACT_PROVIDER_TYPE = symbolFor('react.provider')判断REACT_PROVIDER_TYPE的时候返回 Context.Provider
  • createContext时候,会初始化值,并且挂载到_currentValue_currentValue2上,挂载ProviderConsumer返回context
export function createContext<T>(defaultValue: T): ReactContext<T> {
  // TODO: Second argument used to be an optional `calculateChangedBits`
  // function. Warn to reserve for future use?

  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    // As a workaround to support multiple concurrent renderers, we categorize
    // some renderers as primary and others as secondary. We only expect
    // there to be two concurrent renderers at most: React Native (primary) and
    // Fabric (secondary); React DOM (primary) and React ART (secondary).
    // Secondary renderers store their context values on separate fields.
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    // Used to track how many concurrent renderers this context currently
    // supports within in a single renderer. Such as parallel server rendering.
    _threadCount: 0,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;

  return context;
}

  • 在遇到Provider组件的时候,会把传入的值记录下来
  • 这里面还有一个pushProviderpopProvider的操作
  • render阶段会push,把新值推到栈里,在commit阶段会pop,从栈顶删除值的操作
  • 在遇到Consumer的时候,会从存好的值取出来
  • hooks里面,mountupdate的时候会执行readContext方法读取context的值

为什么会入栈和出栈操作,因为context是跨层级的,在render和commit阶段都会深度遍历节点,这个消耗不少

扩展