重拾React: Context

37次阅读

共计 6353 个字符,预计需要花费 16 分钟才能阅读完成。

前言
首先欢迎大家关注我的 Github 博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励,希望大家多多关注呀!好久已经没写 React,发现连 Context 都发生了变化,忽然有一种村里刚通上的网的感觉,可能文章所提及的知识点已经算是过时了,仅仅算作是自己的学习体验吧,
Context
对于 React 开发者而言,Context 应该是一个不陌生的概念,但是在 16.3 之前,React 官方一直不推荐使用,并声称该特性属于实验性质的 API,可能会从之后的版本中移除。但是在实践中非常多的第三方库都基于该特性,例如:react-redux、mobx-react。

如上面的组件树中,A 组件与 B 组件之间隔着非常多的组件,假如 A 组件希望传递给 B 组件一个属性,那么不得不使用 props 将属性从 A 组件历经一系列中间组件最终跋山涉水传递给 B 组件。这样代码不仅非常的麻烦,更重要的是中间的组件可能压根就用不上这个属性,却要承担一个传递的职责,这是我们不希望看见的。Context 出现的目的就是为了解决这种场景,使得我们可以直接将属性从 A 组件传递给 B 组件。
Legacy Context
这里所说的老版本 Context 指的是 React16.3 之前的版本所提供的 Context 属性,在我看来,这种 Context 是以一种协商声明的方式使用的。作为属性提供者 (Provider) 需要显式声明哪些属性可以被跨层级访问并且需要声明这些属性的类型。而作为属性的使用者 (Consumer) 也需要显式声明要这些属性的类型。官方文档中给出了下面的例子:
import React, {Component} from ‘react’;
import PropTypes from ‘prop-types’;

class Button extends React.Component {

static contextTypes = {
color: PropTypes.string
};

render() {
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
);
}
}

class Message extends React.Component {
render() {
return (
<div>
{this.props.text} <Button>Delete</Button>
</div>
);
}
}

class MessageList extends React.Component {
static childContextTypes = {
color: PropTypes.string
};

getChildContext() {
return {color: “red”};
}

render() {
const children = this.props.messages.map((message) =>
<Message text={message.text} />
);
return <div>{children}</div>;
}
}
我们可以看到 MessageList 通过函数 getChildContext 显式声明提供 color 属性,并且通过静态属性 childContextTypes 声明了该属性的类型。而 Button 通过静态属性 contextTypes 声明了要使用属性的类型,二者通过协商的方式约定了跨层级传递属性的信息。Context 确实非常方便的解决了跨层级传递属性的情况,但是为什么官方却不推荐使用呢?
首先 Context 的使用是与 React 可复用组件的逻辑背道而驰的,在 React 的思维中,所有组件应该具有复用的特性,但是正是因为 Context 的引入,组件复用的使用变得严格起来。就以上面的代码为例,如果想要复用 Button 组件,必须在上层组件中含有一个可以提供 String 类型的 colorContext,所以复用要求变得严格起来。并且更重要的是,当你尝试修改 Context 的值时,可能会触发不确定的状态。我们举一个例子,我们将上面的 MessageList 稍作改造,使得 Context 内容可以动态改变:
class MessageList extends React.Component {

state = {
color: “red”
};

static childContextTypes = {
color: PropTypes.string
};

getChildContext() {
return {color: this.state.color};
}

render() {
const children = this.props.messages.map((message) =>
<Message text={message.text} />
);
return (
<div>
<div>{children}</div>
<button onClick={this._changeColor}>Change Color</button>
</div>
);
}

_changeColor = () => {
const colors = [“red”, “green”, “blue”];
const index = (colors.indexOf(this.state.color) + 1) % 3;
this.setState({
color: colors[index]
});
}
}
上面的例子中我们 MessageList 组件 Context 提供的 color 属性改成了 state 的属性,当每次使用 setState 刷新 color 的时候,子组件也会被刷新,因此对应按钮的颜色也会发生改变,一切看起来是非常的完美。但是一旦组件间的组件存在生命周期函数 ShouldComponentUpdate 那么一切就变得诡异起来。我们知道 PureComponent 实质就是利用 ShouldComponentUpdate 避免不必要的刷新的,因此我们可以对之前的例子做一个小小的改造:
class Message extends React.PureComponent {
render() {
return (
<div>
{this.props.text} <Button>Delete</Button>
</div>
);
}
}
你会发现即使你在 MessageList 中改变了 Context 的值,也无法导致子组件中按钮的颜色刷新。这是因为 Message 组件继承自 PureComponent,在没有接受到新的 props 改变或者 state 变化时生命周期函数 shouldComponentUpdate 返回的是 false,因此 Message 及其子组件并没有刷新,导致 Button 组件没有刷新到最新的颜色。
如果你的 Context 值是不会改变的,或者只是在组件初始化的时候才会使用一次,那么一切问题都不会存在。但是如果需要改变 Context 的情况下,如何安全使用呢?Michel Weststrate 在 [How to safely use React context](https://medium.com/@mweststra…。作者认为我们不应该直接在 getChildContext 中直接返回 state 属性,而是应该像依赖注入(DI) 一样使用 conext。
class Theme {
constructor(color) {
this.color = color
this.subscriptions = []
}

setColor(color) {
this.color = color
this.subscriptions.forEach(f => f())
}

subscribe(f) {
this.subscriptions.push(f)
}
}

class Button extends React.Component {
static contextTypes = {
theme: PropTypes.Object
};

componentDidMount() {
this.context.theme.subscribe(() => this.forceUpdate());
}

render() {
return (
<button style={{background: this.context.theme.color}}>
{this.props.children}
</button>
);
}
}

class MessageList extends React.Component {

constructor(props){
super(props);
this.theme = new Theme(“red”);
}

static childContextTypes = {
theme: PropTypes.Object
};

getChildContext() {
return {
theme: this.theme
};
}

render() {
const children = this.props.messages.map((message) =>
<Message text={message.text} />
);
return (
<div>
<div>{children}</div>
<button onClick={this._changeColor}>Change Color</button>
</div>
);
}

_changeColor = () => {
const colors = [“red”, “green”, “blue”];
const index = (colors.indexOf(this.theme.color) + 1) % 3;
this.theme.setColor(colors[index]);
}
}
在上面的例子中我们创造了一个 Theme 类用来管理样式,然后通过 Context 将 Theme 的实例向下传递,在 Button 中获取到该实例并且订阅样式变化,在样式变化时调用 forceUpdate 强制刷新达到刷新界面的目的。当然上面的例子只是一个雏形,具体使用时还需要考虑到其他的方面内容,例如在组件销毁时需要取消监听等方面。
回顾一下之前版本的 Context,配置起来还是比较麻烦的,尤其还需要在对应的两个组件中分别使用 childContextTypes 和 contextTypes 的声明 Context 属性的类型。而且其实这两个类型声明并不能很好的约束 context。举一个例子,假设分别有三个组件: GrandFather、Father、Son,渲染顺序分别是:
GrandFather -> Father -> Son
那么假设说组件 GrandFather 提供的 context 是类型为 number 键为 value 的值 1,而 Father 提供也是类型为 number 的键为 value 的值 2,组件 Son 声明获得的是类型为 number 的键为 value 的 context,我们肯定知道组件 Son 中 this.context.value 值为 2,因为 context 在遇到同名 Key 值时肯定取的是最靠近的父组件。
同样地我们假设件 GrandFather 提供的 context 是类型为 string 键为 value 的值 ”1″,而 Father 提供是类型为 number 的键为 value 的值 2,组件 Son 声明获得的是类型为 string 的键为 value 的 context,那么组件 Son 会取到 GrandFather 的 context 值吗?事实上并不会,仍然取到的值是 2,只不过在开发过程环境下会输出:
Invalid context value of type number supplied to Son, expected string

因此我们能得出静态属性 childContextTypes 和 contextTypes 只能提供开发的辅助性作用,对实际的 context 取值并不能起到约束性的作用,即使这样我们也不得不重复体力劳动,一遍遍的声明 childContextTypes 和 contextTypes 属性。
New Context
新的 Context 发布于 React 16.3 版本,相比于之前组件内部协商声明的方式,新版本下的 Context 大不相同,采用了声明式的写法,通过 render props 的方式获取 Context,不会受到生命周期 shouldComponentUpdate 的影响。上面的例子用新的 Context 改写为:
import React, {Component} from ‘react’;

const ThemeContext = React.createContext({theme: ‘red’});

class Button extends React.Component {
render(){
return(
<ThemeContext.Consumer>
{({color}) => {
return (
<button style={{background: color}}>
{this.props.children}
</button>
);
}}
</ThemeContext.Consumer>
);
}
}

class Message extends React.PureComponent {
render() {
return (
<div>
{this.props.text} <Button>Delete</Button>
</div>
);
}
}

class MessageList extends React.Component {

state = {
theme: {color: “red”}
};

render() {
return (
<ThemeContext.Provider value={this.state.theme}>
<div>
{this.props.messages.map((message) => <Message text={message.text}/>)}
<button onClick={this._changeColor}>Change Color</button>
</div>
</ThemeContext.Provider>
)
}

_changeColor = () => {
const colors = [“red”, “green”, “blue”];
const index = (colors.indexOf(this.state.theme.color) + 1) % 3;
this.setState({
theme: {
color: colors[index]
}
});
}
}
我们可以看到新的 Context 使用 React.createContext 的方式创建了一个 Context 实例,然后通过 Provider 的方式提供 Context 值,而通过 Consumer 配合 render props 的方式获取到 Context 值,即使中间组件中存在 shouldComponentUpdate 返回 false,也不会导致 Context 无法刷新的问题,解决了之前存在的问题。我们看到在调用 React.createContext 创建 Context 实例的时候,我们传入了一个默认的 Context 值,该值仅会在 Consumer 在组件树中无法找到匹配的 Provider 才会使用,因此即使你给 Provider 的 value 传入 undefined 值时,Consumer 也不会使用默认值。
新版的 Context API 相比于之前的 Context API 更符合 React 的思想,并且能解决 componentShouldUpdate 的带来的问题。与此同时你的项目需要增加专门的文件来创建 Context。在 React v17 中,可能就会删除对老版 Context API 的支持,所以还是需要尽快升级。最后讲了这么多,但是在项目中还是要尽量避免 Context 的滥用,否则会造成组件间依赖过于复杂。

正文完
 0