430 lines
13 KiB
Markdown
430 lines
13 KiB
Markdown
# 4. *Context*
|
|
|
|
有关 *Context*,这是一个非常重要的知识点,甚至我们之后在书写 *mini-react、mini-react-router、mini-redux* 时都会用到的一个知识点,所以这一小节,我们就来好好看一下 *Context* 的相关内容,主要包含以下几个点:
|
|
|
|
- *Context* 要解决的问题
|
|
- *Context* 的用法
|
|
- *Context* 相关 *Hook*
|
|
|
|
## *Context* 要解决的问题
|
|
|
|
首先来看一下 *Context* 要解决的问题。正常来讲,我们单页应用中的组件会形成一个像组件树一样结构,当内部组件和组件之间要进行数据传递时,就免不了一层一层先传递到共同的父组件,然后再一层一层传递下去。
|
|
|
|
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055702.png" alt="image-20221130135702192" style="zoom:50%;" />
|
|
|
|
假设 *subComA-1* 组件的状态数据要传递给 *subComB-2* 组件,应该怎么做?
|
|
|
|
根据我们前面所讲的单项数据流的规则,那么数据应该被提升到 *App* 根组件,然后通过 *props* 一层一层的传递给下面的子组件,最终 *subComA-1* 拿到所需要的数据;如果 *subComA-1* 组件需要修改传递下来的数据,那么该组件就还需接收从 *App* 根组件一层一层传递下来的能够修改数据的方法。
|
|
|
|
官方在“何时使用 *Context*”这一小节也举了一个形象的例子:
|
|
*https://zh-hans.reactjs.org/docs/context.html#when-to-use-context*
|
|
|
|
因此,简单一句话概括 *Context*,那就是解决组件之间数据共享的问题,避免一层一层的传递。
|
|
|
|
此时你肯定会想,前面的 *redux* 不就已经解决了这个问题么?没错,实际上 *redux* 的实现原理就是基于 *Context* 所进行的一层封装。
|
|
|
|
*Context* 如果直接翻译成中文,会被翻译成“上下文”,这其实在软件领域很常见的一个词,比如前面我们也学习过“执行上下文”,所谓上下文,往往指的是代码执行时所需的**数据环境信息**。
|
|
|
|
>实际上生活中也有类似的场景,例如你在厨房做饭,你的周围有各种做饭所需的厨具,例如菜刀、案板、锅、瓢等,这些工具构成了一个(上下文)环境,当你要做饭要用到某一样工具时,直接从这个环境中就能过获取到。
|
|
>
|
|
><img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055752.png" alt="image-20221130135751769" style="zoom:40%;" />
|
|
|
|
## *Context* 的用法
|
|
|
|
*React* 官方对于 *Context* 的用法,分为旧版 *API* 和新版 *API*,有关旧版 *API* 的用法,本文档就不再赘述,如果有需要的同学,可以参阅:*https://zh-hans.reactjs.org/docs/legacy-context.html*
|
|
|
|
这里我们来看一下新版 *API* 的使用,示例如下:
|
|
|
|
```js
|
|
// src/context/index.js
|
|
|
|
import React from "react";
|
|
|
|
const MyContext = React.createContext();
|
|
export default MyContext;
|
|
```
|
|
|
|
首先,使用 *React.createContext API* 创建的一个上下文对象,该对象里面会提供两个组件,分别是 *Provider* 和 *Consumer*,表示数据的提供者和消费者。
|
|
|
|
```js
|
|
// App.jsx
|
|
import ChildCom1 from "./components/ChildCom1";
|
|
import MyContext from "./context";
|
|
import { useState } from "react";
|
|
|
|
const { Provider } = MyContext;
|
|
|
|
function App() {
|
|
const [count, setCount] = useState(0);
|
|
return (
|
|
<Provider value={{ count, setCount }}>
|
|
<div style={{
|
|
border: '1px solid',
|
|
width: "250px",
|
|
}}>
|
|
<ChildCom1 />
|
|
</div>
|
|
</Provider>
|
|
);
|
|
}
|
|
|
|
export default App;
|
|
```
|
|
|
|
在根组件 *App.jsx* 中,我们设置了一个根组件的状态数据 *count*,然后从 *MyContext* 中解构出 *Provider* 组件来作为数据的提供者,*value* 属性用来设置要提供的数据。
|
|
|
|
```js
|
|
// components/ChildCom1.jsx
|
|
|
|
import React from 'react';
|
|
import ChildCom2 from "./ChildCom2"
|
|
import ChildCom3 from "./ChildCom3"
|
|
|
|
function ChildCom1() {
|
|
return (
|
|
<div>
|
|
ChildCom1
|
|
<ChildCom2/>
|
|
<ChildCom3/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ChildCom1;
|
|
```
|
|
|
|
在 *ChildCom1* 子组件中,无需再像中转站一样接受父组件的数据然后又传递给 *ChildCom2* 和 *ChildCom3* 组件。
|
|
|
|
```js
|
|
// components/ChildCom2.jsx
|
|
|
|
import React from 'react';
|
|
import MyContext from "../context";
|
|
|
|
const { Consumer } = MyContext;
|
|
|
|
function ChildCom2() {
|
|
return (
|
|
<Consumer>
|
|
{(context) => (
|
|
<div
|
|
style={{
|
|
border: '1px solid',
|
|
width: "200px",
|
|
userSelect: 'none'
|
|
}}
|
|
onClick={() => context.setCount(context.count + 1)}
|
|
>
|
|
ChildCom2
|
|
<div>count:{context.count}</div>
|
|
|
|
</div>
|
|
)}
|
|
</Consumer>
|
|
);
|
|
}
|
|
|
|
export default ChildCom2;
|
|
```
|
|
|
|
*ChildCom2* 组件是一个函数组件,函数组件想要获取 *Context* 上下文中的数据,需要使用 *Consumer* 组件,这种方法需要一个函数作为子元素,这个函数接收当前的 *context* 值,并返回一个 *React* 节点。
|
|
|
|
```js
|
|
import React, { Component } from 'react'
|
|
import MyContext from "../context";
|
|
|
|
export default class ChildCom3 extends Component {
|
|
|
|
static contextType = MyContext;
|
|
|
|
render() {
|
|
return (
|
|
<div
|
|
style={{
|
|
border: '1px solid',
|
|
width: "200px",
|
|
userSelect: 'none'
|
|
}}
|
|
onClick={()=>this.context.setCount(this.context.count + 2)}
|
|
>
|
|
ChildCom3
|
|
<div>count:{this.context.count}</div>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
*ChildCom3* 组件是一个类组件,类组件当然也可以使用上面 *Consumer* 的方式来获取上下文中的数据,但对于类组件而言,还可以使用 *contextType* 的方式。挂载在 *class* 上的 *contextType* 属性可以赋值为由 *React.createContext( )* 创建的 *Context* 对象。
|
|
|
|
整体的组件树结构图如下:
|
|
|
|
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055829.png" alt="image-20221130135829210" style="zoom:50%;" />
|
|
|
|
最后我们来看一下效果:
|
|
|
|

|
|
|
|
### *displayName*
|
|
|
|
如果安装了 *React Developer Tools* 工具,那么在 *components* 选项卡中可以看到如下的组件树结构,默认的名字就为 *Context.Provider* 和 *Context.Consumer*
|
|
|
|

|
|
|
|
通过设置 *displayName* 可以修改显示名字,如下:
|
|
|
|
```js
|
|
import React from "react";
|
|
|
|
const MyContext = React.createContext();
|
|
MyContext.displayName = 'counter';
|
|
export default MyContext;
|
|
```
|
|
|
|
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055958.png" alt="image-20221130135957667" style="zoom:50%;" />
|
|
|
|
### 默认值
|
|
|
|
*Context* 上下文环境可以设置默认值,如下:
|
|
|
|
```js
|
|
import React from "react";
|
|
|
|
const MyContext = React.createContext({
|
|
name: "xiejie",
|
|
age: 18,
|
|
});
|
|
MyContext.displayName = "counter";
|
|
export default MyContext;
|
|
```
|
|
|
|
此时就**<u>不再需要 *Provider* 组件来提供数据</u>**了,子组件可以直接消费上下文环境的默认数据。
|
|
|
|
```js
|
|
// App.jsx
|
|
|
|
import ChildCom1 from "./components/ChildCom1";
|
|
function App() {
|
|
return (
|
|
<div style={{
|
|
border: '1px solid',
|
|
width: "250px",
|
|
}}>
|
|
<ChildCom1 />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|
|
```
|
|
|
|
根组件 *App* 已经不再需要使用 *Provider* 组件来提供初始数据。
|
|
|
|
```js
|
|
import React from 'react';
|
|
import MyContext from "../context";
|
|
|
|
const { Consumer } = MyContext;
|
|
|
|
|
|
function ChildCom2() {
|
|
return (
|
|
<Consumer>
|
|
{(context) => (
|
|
<div
|
|
style={{
|
|
border: '1px solid',
|
|
width: "200px",
|
|
userSelect: 'none'
|
|
}}
|
|
>
|
|
ChildCom2
|
|
<div>name:{context.name}</div>
|
|
|
|
</div>
|
|
)}
|
|
</Consumer>
|
|
|
|
);
|
|
}
|
|
|
|
export default ChildCom2;
|
|
```
|
|
|
|
```js
|
|
import React, { Component } from 'react'
|
|
import MyContext from "../context";
|
|
|
|
export default class ChildCom3 extends Component {
|
|
|
|
static contextType = MyContext;
|
|
|
|
render() {
|
|
return (
|
|
<div
|
|
style={{
|
|
border: '1px solid',
|
|
width: "200px",
|
|
userSelect: 'none'
|
|
}}
|
|
>
|
|
ChildCom3
|
|
<div>age:{this.context.age}</div>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
无论是 *ChildCom2* 还是 *ChildCom3*,都能够直接从上下文中获取默认的上下文数据。
|
|
|
|
效果如下:
|
|
|
|
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-060026.png" alt="image-20221130140025242" style="zoom:67%;" />
|
|
|
|
### 多个上下文环境
|
|
|
|
在上面的示例中,我们示例的都是一个 *Context* 上下文环境,这通常也够用了,但是这并不意味着只能提供一个上下文环境,我们可以创建多个上下文环境,示例如下:
|
|
|
|
```js
|
|
import React from "react";
|
|
|
|
export const MyContext1 = React.createContext();
|
|
export const MyContext2 = React.createContext();
|
|
```
|
|
|
|
首先,我们导出两个上下文环境,接下来在 *App.jsx* 中,使用多个 *Provider* 作为数据的提供者
|
|
|
|
```js
|
|
import ChildCom1 from "./components/ChildCom1";
|
|
import { MyContext1, MyContext2 } from "./context"
|
|
function App() {
|
|
return (
|
|
<MyContext1.Provider value={{ a: 1, b: 2 }}>
|
|
<MyContext2.Provider value={{ b: 10, c: 20 }}>
|
|
<div style={{border: '1px solid',width: "250px",}}>
|
|
<ChildCom1 />
|
|
</div>
|
|
</MyContext2.Provider>
|
|
</MyContext1.Provider>
|
|
|
|
);
|
|
}
|
|
|
|
export default App;
|
|
```
|
|
|
|
之后在 *ChildCom2* 和 *ChildCom3* 中同样也使用多个 *Consumer* 来消费不同上下文中的数据
|
|
|
|
```js
|
|
import React from 'react';
|
|
import { MyContext1, MyContext2 } from "../context";
|
|
function ChildCom2() {
|
|
return (
|
|
<MyContext1.Consumer>
|
|
{
|
|
(context1) => (
|
|
<MyContext2.Consumer>
|
|
{(context2) => (
|
|
<div
|
|
style={{
|
|
border: '1px solid',
|
|
width: "200px",
|
|
userSelect: 'none'
|
|
}}
|
|
>
|
|
ChildCom2
|
|
<div>a : {context1.a}</div>
|
|
<div>b : {context1.b}</div>
|
|
<div>c : {context2.c}</div>
|
|
</div>
|
|
)}
|
|
</MyContext2.Consumer>
|
|
)
|
|
}
|
|
</MyContext1.Consumer>
|
|
);
|
|
}
|
|
|
|
export default ChildCom2;
|
|
```
|
|
|
|
```js
|
|
import React, { Component } from 'react'
|
|
import { MyContext1, MyContext2 } from "../context";
|
|
|
|
export default class ChildCom3 extends Component {
|
|
render() {
|
|
return (
|
|
<MyContext1.Consumer>
|
|
{
|
|
(context1) => (
|
|
<MyContext2.Consumer>
|
|
{
|
|
(context2) => (
|
|
<div
|
|
style={{
|
|
border: '1px solid',
|
|
width: "200px",
|
|
userSelect: 'none'
|
|
}}
|
|
>
|
|
ChildCom3
|
|
<div>a:{context1.a}</div>
|
|
<div>b:{context2.b}</div>
|
|
<div>c:{context2.c}</div>
|
|
</div>
|
|
)
|
|
}
|
|
</MyContext2.Consumer>
|
|
)
|
|
}
|
|
</MyContext1.Consumer>
|
|
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
效果如下:
|
|
|
|
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-060047.png" alt="image-20221130140046232" style="zoom:67%;" />
|
|
|
|
>在消费上下文里面的数据时,如果回调函数中的参数名相同,则从最近的上下文中去获取数据。
|
|
|
|
## *Context* 相关 *Hook*
|
|
|
|
在 *React Hook API* 中,为我们提供了一个更加方便的 *useContext* 钩子函数,该 *Hook* 接收一个由 *React.createContext API* 创建的上下文对象,并返回该 *context* 的当前值。
|
|
|
|
例如:
|
|
|
|
```js
|
|
import { useContext } from 'react';
|
|
import { MyContext1 } from "../context";
|
|
function ChildCom2() {
|
|
const { a, b, c } = useContext(MyContext1)
|
|
return (
|
|
<div
|
|
style={{
|
|
border: '1px solid',
|
|
width: "200px",
|
|
userSelect: 'none'
|
|
}}
|
|
>
|
|
ChildCom2
|
|
<div>a : {a}</div>
|
|
<div>b : {b}</div>
|
|
<div>c : {c}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ChildCom2;
|
|
```
|
|
|
|
*useContext(MyContext)* 相当于类组件中的 *static contextType = MyContext* 或者 \<*MyContext.Consumer*>,但是我们**<u>仍然需要在上层组件树中使用 \<*MyContext.Provider*> 来为下层组件提供 *context*</u>**。
|
|
|
|
---
|
|
|
|
-*EOF*-
|