前言
createContext是React提供的一个全局的状态管理的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.Provider的value发生变化,重新渲染的时候,它里面的组件都会重新渲染- 内部会转化成
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.ConsumerREACT_PROVIDER_TYPE = symbolFor('react.provider')判断REACT_PROVIDER_TYPE的时候返回Context.ProvidercreateContext时候,会初始化值,并且挂载到_currentValue和_currentValue2上,挂载Provider和Consumer返回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组件的时候,会把传入的值记录下来 - 这里面还有一个
pushProvider和popProvider的操作 - 在
render阶段会push,把新值推到栈里,在commit阶段会pop,从栈顶删除值的操作 - 在遇到
Consumer的时候,会从存好的值取出来 - 在
hooks里面,mount和update的时候会执行readContext方法读取context的值
为什么会入栈和出栈操作,因为context是跨层级的,在render和commit阶段都会深度遍历节点,这个消耗不少