我们知道在react当中,组件与组件的沟通是通过props,父组件通过props给子组件传递一些属性,父组件可以传递一些回调函数给子组件,让子组件在某些特定的时候,可以调用父组件的一些特性。
但是我们会存在一个问题就是,react 的应用当中,组件和组件之间并不一定只有父子关系。他们还会存在着父子嵌套多层之后,第一层和最后一层之间是一个祖孙的关系。他们之间会隔着好几层不同的组件。那如果他们之间要进行一些沟通,我们通过 props 传递是不太现实的。因为中间的那几层组件不一定是我们自己写的。而且中间这个组件要去传递这个 props ,对她来说是完全没有意义的事情,所以这个时候 react 就给我们提供了 context 的使用方式
上级组件当中我们提供了一个 context 对象之后,他的子元素里面都可以通过 context 去获取到他提供的这部分内容,以此达到一个跨越多层组件传递信息的一个功能。
context有两种实现方式
1、childContextType (即将在 react 17 版本的时候废弃)
2、createContext (react 16 之后提供的)
代码
import React from 'react' import PropTypes from 'prop-types' const { Provider, Consumer } = React.createContext('default') class Parent extends React.Component { state = { childContext: '123', newContext: '456', } getChildContext() { return { value: this.state.childContext, a: 'aaaaa' } } render() { return ( <> <div> <label>childContext:</label> <input type="text" value={this.state.childContext} onChange={e => this.setState({ childContext: e.target.value })} /> </div> <div> <label>newContext:</label> <input type="text" value={this.state.newContext} onChange={e => this.setState({ newContext: e.target.value })} /> </div> <Provider value={this.state.newContext}>{this.props.children}</Provider> </> ) } } class Parent2 extends React.Component { // { value: this.state.childContext, a: 'bbbbb' } getChildContext() { return { a: 'bbbbb' } } render() { return this.props.children } } function Child1(props, context) { console.log(context) return <Consumer>{value => <p>newContext: {value}</p>}</Consumer> } Child1.contextTypes = { value: PropTypes.string, } class Child2 extends React.Component { render() { return ( <p> childContext: {this.context.value} {this.context.a} </p> ) } } // Child2.contextType = Consumer Child2.contextTypes = { value: PropTypes.string, a: PropTypes.string, } Parent.childContextTypes = { value: PropTypes.string, a: PropTypes.string, } Parent2.childContextTypes = { a: PropTypes.string, } export default () => ( <Parent> <Parent2> <Child1 /> <Child2 /> </Parent2> </Parent> )
第一种,childContextType
export default () => ( <Parent> <Parent2> <Child2 /> </Parent2> </Parent> )
首先组件层级是这样的,这里 child2 子级要使用 parent 的数据。要使用 childContextType,必须先声明
Parent.childContextTypes = {
value: PropTypes.string,
a: PropTypes.string,
}
然后在 Parent 组件里面的 getChildContext 方法可以设置数据
getChildContext() { return { value: this.state.childContext, a: 'aaaaa' } }
设置好之后,在 Child2 里面使用 childContextType,也需要先声明
Child2.contextTypes = {
value: PropTypes.string,
a: PropTypes.string,
}
声明好之后,则可以在 Child2 里面使用
render() { return ( <p> childContext: {this.context.value} {this.context.a} </p> ) }
为什么一定要这么做呢?因为在 react 中,Child2 的上层组件不一定只有一个,他上层组件提供的 context 也不一定只有一个。他们是有一个 merge 的关系的。所以 react 需要知道你获取的是 context 的哪几个属性。这是老的 context 的用法
第二种,createContext
新的context,是通过 React.createContext,他返回一个对象,这个对象包含 Provider , Consumer
const { Provider, Consumer } = React.createContext('default')
分别是 context 的提供方 和 context 的订阅方。那么这两个都是组件
<Provider value={this.state.newContext}>{this.props.children}</Provider>
我们通过 Provider 这个组件,然后上面指定 value。这个 value 就是这个 context 他对应的属性。在他的子组件下面,只需要去通过 Consumer 这个组件,然后传入的是一个方法,这个方法我们认为是 function component 。他接收这个 value,并且把想要渲染的东西渲染出来就可以了
<Consumer>{value => <p>newContext: {value}</p>}</Consumer>
所以 Provider 和 Consumer 是一一对应的关系。在上层组件用到之后,在子组件里面什么时候用到,就什么时候用 Consumer 去获取
那么 react 为什么要弃用老的 api, 使用这种新的 api 呢,因为老的 api 他对于 context 的提供方他下层的影响太大了。他会导致他下层的所有组件,即便是没有任何更新的情况下 他每次更新的过程中仍然要进行完整的渲染。所以对性能的损耗非常大。
这就是我们 context 的两种方式,接下来我们来看下 createContext 的源码,打开 RreateContext
import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; import warningWithoutStack from 'shared/warningWithoutStack'; import warning from 'shared/warning'; export function createContext<T>( defaultValue: T, calculateChangedBits: ?(a: T, b: T) => number, ): ReactContext<T> { if (calculateChangedBits === undefined) { calculateChangedBits = null; } else { if (__DEV__) { warningWithoutStack( calculateChangedBits === null || typeof calculateChangedBits === 'function', 'createContext: Expected the optional second argument to be a ' + 'function. Instead received: %s', calculateChangedBits, ); } } const context: ReactContext<T> = { $$typeof: REACT_CONTEXT_TYPE, _calculateChangedBits: calculateChangedBits, _currentValue: defaultValue, _currentValue2: defaultValue, // These are circular Provider: (null: any), Consumer: (null: any), }; context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, }; context.Consumer = context; return context; }
我们看到 createContext ,他接收的是一个 defaultValue ,还有一个是 calculateChangedBits 。是一个方法,是用来计算新老 context 的一个变化的。方法里面看到他声明了一个 context 对象,这个对象跟之前看的 ReactElement 非常的像。也有一个 $$typeof ,这个 $$typeof 跟 ReactElement 的 $$typeof 是不一样的。还有两个属性 _currentValue , _currentValue2 这两个属性是一样的,只是用到的地方不一样。 _currentValue 这个 value 是用来记录 Provider 里面的这个 value 。 他有变化的情况下就会更新到这个 _currentValue 。这就是用来记录最新的 context 的值的, 下面会有个 Provider 和 Consumer 。
下面有个 context.Provider ,他有个属性 _context ,这个属性会指向这个 context。context.Consumer = context。 也就是说 Consumer 是等于自己的。
到这我们知道 Consumer 就是指向这个对象的时候,可以猜到,在 Consumer 进行渲染的时候,他要去获取这个 value 怎么办呢,他只要从自己本身上面 _currentValue 就可以拿到最新的 context 的 value 。然后再调用那个方法把他传进去。这就是基本的实现原理,真正的实现没有这么简单。
这里面的 $$typeof 并不是要替换 ReactElement 的 $$typeof。而是这里面的 $$typeof 是指 ReactElement 里面的 type.