前提环境
比如说,项目的组件结构如下图,在 App
组件中有一个数据为 loginUser
,而需要在 Child1
组件中使用这个数据,那么在使用上下文之前,我们就只能通过 props 一层一层地往子组件传递,当这个层级很深的时候,属性的传递就会变得非常繁琐;而且如果属性名变更或者新增数据传递,那可能需要在每一层都去改动这个相关的数据传递。
所以,为了解决这个问题,就出现了上下文。图示如下图,黄色背景即为 App 组件创建的上下文,那么这个组件的后代组件都可以共享这个上下文中存储的数据。
上下文的特点:
-
当某个组件创建了上下文后,上下文中存储的数据,会被所有的后代组件共享
-
如果某个组件依赖了上下文,会导致该组件的数据来源不再纯粹 (一般认为外部数据仅来源于 props 的组件为纯粹组件),所以尽量谨慎使用
-
一般用于第三方通用组件
旧版 API
因为保不齐需要维护旧版 React 开发的项目,所以旧版 API 也需要了解
上下文的创建
只有 类组件
才可以创建上下文
-
给类组件书写
静态属性
childContextTypes
,使用该属性对上下文中的数据类型进行约束 -
添加实例方法
getChildContext
,此方法返回的对象,即为上下文中的数据,该数据必须满足类型约束 -
每次均会在
render
之后调用,新版本写法已废弃;如今在<React.StrictMode>
节点下使用会报警告 (API 已过时):Warning: Legacy context API has been detected within a strict-mode tree.
import React, {Component} from 'react' import PropTypes from 'prop-types' export default class OldContext extends Component { static childContextTypes = { a: PropTypes.number, b: PropTypes.string.isRequired, addToA: PropTypes.func } state = { a: 1 } /* 此函数会在 render 后自动调用 */ getChildContext() { console.log('getChildContext 获取上下文数据') return { a: this.state.a, b: 'K', /* 提供更改上下文数据的方法 */ addToA: n => { this.setState({ a: this.state.a + n }) } } } render() { // 自身获取上下文数据 // const {a, b} = this.getChildContext() return ( <div>OldContext</div> ) } }
-
以上面的图示为例,如果
ChildA
也创建了一个上下文,并且跟App
组件创建的上下文有重名属性,那么它们的属性类型约束必须保持一致;ChildA
通过this.context
取这个同名属性的数据时,会获取到其父级上下文中的数据
,而不是自身创建的上下文数据;但当后代获取上下文数据时,就类似作用域链取数据的方式,获取最近的上下文
中的数据。
上下文的使用
-
自身组件非要这么使用的话:
const ctxData = this.getChildContext()
【没什么话说】 -
如果要使用上下文中的数据,组件 (函数/类) 必须拥有一个
静态属性 contextTypes
,该属性描述了需要获取的上下文中获取数据的类型- 类组件: 可以从
constructor
中的第二个参数获取;或者使用this.context
从属性中获取,使用constructor
时,要在constructor(props, context)
构造函数中调用super(props, context)
交给父类处理,就会将context
注入到属性中;不使用constructor
时会默认注入
// 以上面提供的上下文数据为例 import React, {Component} from 'react' import PropTypes from 'prop-types' // ChildComp 组件是上面 OldContext 组件的子组件或更深的子组件 export default class ChildComp extends Component { // 声明这个组件需要使用的上下文数据;声明哪些,就只能获取哪些 static contextTypes = { a: PropTypes.number // 类型不匹配,会输出警告信息 } constructor(props, context){ super(props) console.log(context) // 这里只会拿到 {a: 1} } render() { return ( <div>ChildComp</div> ) } }
- 函数组件: 通过
第二个参数
获取上下文数据
// 还是以上面提供的上下文数据为例 import React, {} from 'react' import PropTypes from 'prop-types' export default function ChildComp (props, ctx) { console.log(ctx) // 拿到 a 与 addToA return ( <div> ChildComp <p>a: {args[0].a}</p> <button onClick={() => { ctx.addToA(5) }}>a + 5</button> </div> ) } ChildComp.contextTypes = { a: PropTypes.number, // 类型不匹配,会输出警告信息 addToA: PropTypes.func }
- 类组件: 可以从
函数组件使用上下文与 ref 转发的冲突
上面函数组件通过第二个参数获取上下文,昨天刚好有写 ref 使用与转发的文章:函数组件转发的 ref 也是通过函数的第二个参数接收处理的,那么肯定存在冲突的问题
。经过测试得知如果函数组件同时使用 上下文
与 ref 转发
的情况下,转发的 ref 对象将会作为第二个参数传递给这个函数组件,这个组件就无法获得上下文数据了。测试代码如下:
import React, { Component, forwardRef, createRef } from 'react'
import PropTypes from 'prop-types'
function CompA(props, ref,...args) {
console.log('ref: ', ref) // 打印 ref: {current: null} => divDOM
console.log('CompA args: ', args) // 打印 []
return <div ref={ref}>CompA</div>
}
CompA.contextTypes = {
a: PropTypes.number
}
const RefCompA = forwardRef(CompA) // ref 转发组件 A
function CompB(props, ...args) {
console.log('CompB args: ', args) // 打印 [{a: 1, changeA: func}]
return (
<div>
CompB<br/>
<p>a: {args[0].a}</p>
<button onClick={() => { args[0].changeA() }}>a + 1</button>
</div>
)
}
CompB.contextTypes = {
a: PropTypes.number,
changeA: PropTypes.func
}
export default class OldContext extends Component {
static childContextTypes = {
a: PropTypes.number,
b: PropTypes.string.isRequired,
changeA: PropTypes.func
}
state = {
a: 1
}
getChildContext() {
return {
a: this.state.a,
b: 'K',
changeA: () => { this.setState({ a: this.state.a + 1 }) }
}
}
compRef = createRef() // 创建 ref 对象
render() {
console.log('render', this.compRef) // 打印 {current: null} => divDOM
return (
<div>
OldContext
<RefCompA ref={this.compRef}/>
<CompB />
</div>
)
}
}
新版 API
由于旧版 API 存在比较严重的效率问题,并且容易导致滥用。所以,在新版 API 中,就将上下文抽离为一个独立于组件的对象
上下文的创建
借助 React.createContext()
创建上下文,参数即为上下文的默认值,返回一个包含 Provider (上下文生产者)
和 Consumer (上下文消费者)
组件的对象
import React, {Component, createContext} from 'react'
// ts 中参数必填(我只尝试了 ts),一般抽离为一个文件导出
const ctx = createContext({})
export default class NewContext extends Component {
state = {
a: 1,
b: 'K'
}
render() {
{/* 必填 value 属性 */}
return (
<ctx.Provider value={this.state}>
<div>NewContext</div>
</ctx.Provider>
)
}
}
上下文的使用
-
在类组件中
-
使用
静态属性 contextType
(注意:type 没有s
),且应该赋值为创建的上下文对象,直接使用this.context
使用上下文数据 -
不使用
静态属性 contextType
,则可以直接使用ctx.Consumer
组件的方式,子节点使用函数渲染即可 (写法可见下面函数组件)
import React, {Component, createContext} from 'react' const ctx = createContext({}) class ChildComp extends Component { static contextType = ctx render() { return ( <div> 子组件,上下文数据:a = {this.context.a},b = {this.context.b} </div> ) } } // class ChildComp extends Component { // render() { // return ( // <ctx.Consumer> // { // context => (<div> // 子组件,上下文数据: // a = {context.a}, // b = {context.b} // </div>) // } // </ctx.Consumer> // ) // } // } export default class NewContext extends Component { state = { a: 1, b: 'K' } render() { return ( <ctx.Provider value={this.state}> <div> NewContext <ChildComp /> </div> </ctx.Provider> ) } }
-
-
在函数组件中,需要借助
Consumer 组件
来使用上下文数据,它的子节点children
是一个函数,这个函数的参数即为上下文的数据 (或者使用后面 Hook 文章中的useContext Hook
)import React, {Component, createContext} from 'react' const ctx = createContext({}) function ChildComp (props) { render() { return ( <div> <ctx.Consumer> {val => <span>a: {val.a}</span>} </ctx.Consumer> </div> ) } } export default class NewContext extends Component { state = { a: 1, b: 'K' } render() { return ( <ctx.Provider value={this.state}> <div> NewContext <ChildComp /> </div> </ctx.Provider> ) } }
注意点
如果上下文提供者 (Context.Provider
) 中的 value 属性发生变化,默认情况下会导致该上下文提供的所有后代元素全部重新渲染,若该子元素 未使用
上下文数据则可优化 (比如 shouldComponentUpdate
返回 false
可以跳过重新 render) 。若后代元素使用了上下文,则无论是否有优化,一定会重新渲染 (且会跳过 shouldComponentUpdate
函数的运行)
import React from 'react'
import ctx from './ctx'
class ContextComp extends React.Component {
state = {
a: 1,
changeA: () => {
this.setState({
...this.state,
a: this.state.a + 1
})
}
}
render() {
console.log('ContextComp render')
return (
<ctx.Provider value={this.state}>
<div>
<ChildComp />
</div>
</ctx.Provider>
)
}
}
class ChildComp extends React.Component {
shouldComponentUpdate() {
console.log('ChildComp shouldComponentUpdate => false')
return false
}
render() {
console.log('ChildComp render')
return (
<>
ChildComp
<GrandSonComp />
</>
)
}
}
class GrandSonComp extends React.Component {
shouldComponentUpdate() {
console.log('GrandSonComp shouldComponentUpdate => false')
return false
}
render() {
console.log('GrandSonComp render')
return (
<ctx.Consumer>
{
context => (
<div>
<p>{context.a}</p>
<button
onClick={() => context.changeA()}
>更新 context 的 a</button>
</div>
)
}
</ctx.Consumer>
)
}
}
如上面这个栗子,点击 GrandSonComp
组件的按钮 更新 context 的 a
,你会发现 GrandSonComp
组件内部 a 的值已更新,但控制台打印结果如下:
'ContextComp render'
'ChildComp shouldComponentUpdate =>' false
我们发现,在 ChildComp
的生命周期函数中 shouldComponentUpdate
返回 false
会导致它本身及 GrandSonComp
组件均不会重新渲染,但 ctx.Consumer
组件数据变更则会导致其本身及其子组件重新渲染 (表现为页面显示的 a 刷新)
上面的 GrandSonComp
组件稍作修改:
// ......
class GrandSonComp extends React.Component {
shouldComponentUpdate() {
console.log('GrandSonComp shouldComponentUpdate => false')
return false
}
static contextType = ctx // 静态属性使用方式
handleClick = () => {
this.context.changeA()
}
render() {
console.log('GrandSonComp render')
return (
<>
<p>{this.context.a}</p>
<p>
<button onClick={this.handleClick}>更新 ctx.a</button>
</p>
</>
)
}
}
点击按钮打印结果如下:
'ContextComp render'
'ChildComp shouldComponentUpdate =>' false
'GrandSonComp render'
我们看到,GrandSonComp
组件重新渲染,而且是直接跳过它的 shouldComponentUpdate
函数直接渲染