目录
- 1,介绍
- 2,创建和使用
- 3,注意点
-
- 3.1,在后代中更改上下文
- [3.2,Provider 禁止多组件使用](#3.2,Provider 禁止多组件使用)
- 3.3,后代组件始终重新渲染
1,介绍
React 中的 context
类似与 Vue 中的 project/inject
,祖先提供数据,可以在后代组件中共享。
特点:
- 当某个组件创建了上下文,则上下文中的所有数据,可以被后代组件共享。
- 如果某个组件依赖了上下文,会导致该组件不再纯粹(也就是说,外部数据不止来源于
props
)。 - 使用场景,一般是在第三方组件中。
2,创建和使用
2.1,创建
上下文数据是一个独立于组件的对象,创建:
js
const ctx = React.createContext()
返回值主要有2个重要的属性(使用了一种开发模式:生产消费模式):
Provider
生产者。它是一个组件,该组件会创建上下文,并且有一个value
属性,通过该属性来传递数据。
js
import React, { Component } from "react";
const ctx = React.createContext();
export default class App extends Component {
state = {
a: 0,
b: "abc",
changeA: (newA) => {
this.setState({
a: newA,
});
},
};
render() {
return (
<ctx.Provider value={this.state}>
<ChildA></ChildA>
</ctx.Provider>
);
}
}
Consumer
使用者,用于在后代组件中使用上下文。
2.2,后代组件中使用
2.2.1,函数组件
只能使用上下文对象的 Consumer
属性来获取上下文数据。
js
function ChildB(props) {
return (
<ctx.Consumer>
{(value) => (
<div>
<div>组件B</div>
<div>
a:{value.a},b:{value.b}
</div>
<button
onClick={() => {
value.changeA(value.a + 1);
}}
>
在后代组件中更改上下文的数据,a+1
</button>
</div>
)}
</ctx.Consumer>
);
}
注意 :Consumer
是一个组件,它的子节点 是一个函数,会将上下文数据通过函数的参数提供出来。该函数的返回值会进行渲染。
也就是说,它的
props.children
需要传递一个函数。
2.2.2,类组件
有2种使用方式:
1,使用 Consumer
属性来使用上下文。
2,首先必须 添加一个静态属性 contextType
来标记要使用的上下文。之后可通过 this.context.xxx
使用上下文。
js
class ChildA extends Component {
static contextType = ctx;
render() {
return (
<h1>
<div>组件A</div>
<div>
a:{this.context.a},b:{this.context.b}
</div>
<button
onClick={() => {
this.context.changeA(this.context.a + 1);
}}
>
在后代组件中更改上下文的数据,a+1
</button>
</h1>
);
}
}
3,注意点
3.1,在后代中更改上下文
上面的示例代码中,已经提到了,不多赘述。
3.2,Provider 禁止多组件使用
这个多组件,指的同级组件。比如,如果 A和B是同级,并且都有后代组件,那最好不要共用一个上下文数据。
换句话说:下面2个是一一对应 的,某个组件通过 Provider
提供的上下文 ctx
,只有它的后代组件才能使用。
js
const ctx = React.createContext();
<ctx.Provider value={}>
这种场景有2种解决方式:
1,要么将该上下文数据提升到A和B的父级。
2,要么重新创建一个上下文,让A和B互不影响。
3.3,后代组件始终重新渲染
如果上下文的提供者 Context.Provider
的 value
属性发生变化,会导致该上下文提供的所有后代元素全部重新渲染 ,子组件会强制执行 render
。
之前在生命周期中提到,只有当
shouldComponentUpdate()
返回 true 时才会执行render
。但在上下文中的这种情况,
shouldComponentUpdate
不会执行 ,直接跳过它来执行render
,这种情况叫强制渲染。
来看下面的例子:
js
export default class App extends Component {
state = {
a: 0,
b: "abc",
changeA: (newA) => {
this.setState({
a: newA,
});
},
};
render() {
return (
<ctx.Provider value={this.state}>
<ChildA></ChildA>
<ChildB></ChildB>
<button
onClick={() => {
this.setState({});
}}
>
修改 state
</button>
</ctx.Provider>
);
}
}
ChildA
和 ChildAB
的代码上文有,这里补充一些,其他代码省略了。
js
class ChildA extends Component {
shouldComponentUpdate(nextProps, nextState) {
console.log("运行了优化");
return false;
}
render() {
console.log('renderA');
return ({/* ... */});
}
}
function ChildB(props) {
console.log("B函数再次执行");
return (
<ctx.Consumer>
{/* ... */}
</ctx.Consumer>
);
}
不管是祖先组件中执行 this.setState({})
,还是在后代组件中调用 changeA
方法(间接执行祖先组件的 this.setState({})
),都会导致Context.Provider
的 value
属性发生变化。
可以看到不管点击祖先组件中的按钮,还是后代组件中的按钮,每次点击,3个 console.log
只有 shouldComponentUpdate
中的没有执行。
另外,看到祖先组件中写的是
this.setState({})
,传递的是空对象,但也会触发状态更新。因为新旧
state
是通过Object.is()
来比较的,每次会用新的state
对象覆盖旧的state
对象。所以Context.Provider
的value
属性还是发生了变化。
但是在开发中,类组件有时还是需要通过 shouldComponentUpdate
来控制是否执行 render
。
解决 shouldComponentUpdate 不执行问题:
知道 this.setState({})
的更新原理之后,可以将要传递的上下文对象包装一层即可。
这样即便 state
被替换,原来的对象属性还是在内存中没有发生变化。
js
export default class App extends Component {
state = {
ctx: {
a: 0,
b: "abc",
changeA: (newA) => {
this.setState({
a: newA,
});
},
},
};
render() {
return (
<ctx.Provider value={this.state.ctx}>
{/* 其他代码不变 */}
</ctx.Provider>
);
}
}
以上。