内容简介:首先欢迎大家关注我的对于React开发者而言,Context应该是一个不陌生的概念,但是在16.3之前,React官方一直不推荐使用,并声称该特性属于实验性质的API,可能会从之后的版本中移除。但是在实践中非常多的第三方库都基于该特性,例如:react-redux、mobx-react。
前言
首先欢迎大家关注我的 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
类型的color Context
,所以复用要求变得严格起来。并且更重要的是,当你尝试修改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的滥用,否则会造成组件间依赖过于复杂。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。