很久不见!(两个多月没更新内容,惭愧了三分钟)。接下来的文章主要是开始对react的内容作一些整理(疯狂立Flag)。本文的对象是Context
.react
在React中,数据传递通常使用props传递数据,维持单向数据流,这样可让组件之间的关系变得简单且可预测,可是单项数据流在某些场景中并不适用,看一个官方给出的例子:
有三个组件APP
, Toolbar
,ThemedButton
,关系如图:(为了方便你们理解(偷懒),这个例子我会全文通用。)编程
APP
存放着主题相关的参数theme
,须要传递组件ThemedButton
, 若是考虑使用props
,那么代码就长这样:api
class App extends React.Component { render() { return <Toolbar theme="dark" />; // 1. 将theme传递给 } } function Toolbar(props) { // Toolbar 组件接受一个额外的“theme”属性,而后传递给 ThemedButton 组件。 return ( <div> <ThemedButton theme={props.theme} /> // 2. 继续往下传递给Button </div> ); } class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; // 最终获取到参数 } }
能够看到,实际上须要参数的是组件ThemedButton
,可是却必须经过Toolbar
做为中介传递。不妨再引伸思考一下:数据结构
ThemedButton
并不是末级子节点,那么参数必须继续向下传递App
中,还有除了<ThemedButton>
之外的组件,也须要theme
参数,那么也必须按照这种形式逐层传递那么数据结构图大概如图所示:react-router
结构图placeholder:层层传递ide
显然,这样作太!繁!琐!了!函数式编程
接下来,就要介绍今天的主角--Context函数
Context 提供了一种在组件之间共享此类值的方式,而没必要显式地经过组件树的逐层传递 props。
上面是官方对于context的介绍,简单来讲,就是能够把context
当作是特定一个组件树内共享的store,用来作数据传递。
为何这里要加粗强调组件树呢?由于它是基于树形结构共享的数据:在某个节点开启提供context后,全部后代节点compoent均可以获取到共享的数据。this
语言描述略显抽象,直接上代码:spa
如下介绍的是在react 16.x之前的传统写法
class App extends React.Component { // 核心代码1: 首先在提供context的组件(即provider)里 使用`getChildContext`定义要共享给后代组件的数据,同时使用`childContextTypes`作类型声明 static childContextTypes = { theme: PropTypes.string }; getChildContext () { return { theme: 'dark' } } render() { return <Toolbar />; // 无需再将theme经过props传递 } } function Toolbar(props) { return ( <div> <ThemedButton /> // Toolbar 组件再也不接受一个额外的“theme”属性 </div> ); } // 核心代码2: 而后在须要使用context数据(即consumer)的节点,用`contextTypes`声明须要读取的context属性,不然读不到text class ThemedButton extends React.Component { static contextTypes = { theme: PropTypes.string } render() { return <h2>{this.context.theme}</h2>; // 直接从context获取到参数 为了直观 这里改用<h2>直接显示出来 } }
这个结构图就不画了,显然,就是把theme
从层层传递的props
中解放出来了。
在代码中咱们提到了provider
和consumer
,这里简单解释下:context
使用的生产者provider
- 消费者consumer
模式,
context
的叫作provider
,好比例子中的APP
,context
的称为consumer
,对应例子中的ThemedButton
。若是咱们在APP
组件提供了一个切换主题的按钮,那就须要context
可以更新而且通知到相应的consumer
。
因为context
自己提供了相关功能:
getChildContext
方法在每次state
和props
改变时会被调用;provider
改变了context
,全部的后代组件中的consumer
都会从新渲染。因此一般的方式是:将context
的数据保存在Provide
的state
属性中,每次经过setState
更新对应的属性时。
class App extends React.Component { static childContextTypes = { theme: PropTypes.string }; constructor(props) { super(props); this.state = {theme:'dark'}; } getChildContext () { return { theme: this.state.theme // 核心代码,将`context`的值保存在`state` } } render() { return <Toolbar />; } }
可是官方文档同时提到了这种方法是有隐患的,下一节进行详细解析。
shouldComponentUpdate
再次强调,如下介绍的是在react 16.x之前的版本,关于context新的api会在后面介绍
官方文档提到:
The problem is, if a context value provided by component changes, descendants that use that value won’t update if an intermediate parent returns false from shouldComponentUpdate.
(皇家翻译上场) 拿前面的例子来讲,咱们在第二节经过使用context
,将theme
的传递方式由本来的APP
->Toolbar
->ThemedButton
经过props
层层传递变成:
可是组件自己的层级关系依然是APP
->Toolbar
->ThemedButton
。若是咱们在中间层Toolbar
()
的生命周期shouldComponent
返回false
会怎么样呢?接下来咱们针对Toolbar
作一些改动
// 旧写法 function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } // 新写法 使用PureComponent render内容同样, // PS:PureComponent内置的shouldComponentUpdate对state和props作了浅比较,这里为了省事直接使用 //若是不熟悉PureComponent能够直接用React.Component,而后补上shouldComponentUpdate里的 浅比较判断 class Toolbar extends React.PureComponent { render(){ return ( <div> <ThemedButton /> </div> ); } }
这里为了省事,咱们直接使用了PureComponent
,接下来会发现:
每次APP
更新theme
的值时,ThemedButton没法再取到变动后的theme
新的结构图是这样的(注意红线表示来自toolbar
的抵抗):
如今问题来了:
因为Toolbar
组件是PureComponent
,没法重写shouldComponentUpdate
,这就意味着位于Toolbar
以后的后代节点都没法获取到context
的更新!
第一种思路:首先,咱们先看看问题的根源之一,是context
更新以后,后代节点没法及时获取到更新,那么若是context不发生更,那就不存在这个问题了.【我我的以为这个思路有点相似于,解决不了问题,能够考虑解决提出问题的人】,也就意味着:
immutable
constructor
函数中获取一次context
。context
中保存具体的状态值,而是只利用它作个依赖注入。绕开SCU(shouldComponentUpdate)
,从根本上解决问题。 例如,能够经过发布订阅模型建立一个自我管理的ThemeManage
类来解决问题。具体实现以下:// 核心代码 class ThemeManager { constructor(theme) { this.theme = theme this.subscriptions = [] } // 变动颜色时 提示相关的订阅者 setColor(theme) { this.theme = theme this.subscriptions.forEach(f => f()) } // 订阅者接收到响应 触发对应的callbck保证本身的及时更新 subscribe(f) { this.subscriptions.push(f) } } class App extends React.Component { static childContextTypes = { themeManager: PropTypes.object // 本次经过context传递一个theme对象 }; constructor(props) { super(props); this.themeManager = new ThemeManager('dark') // 核心代码 } getChildContext () { return {theme: this.themeManager} // 核心代码 } render() { return <Toolbar />; } } // Toolbar依然是个PureComponent class Toolbar extends React.PureComponent { render(){ return ( <div> <ThemedButton /> </div> ); } } class ThemedButton extends React.Component { constructor(){ super(); this.state = { theme: theme:this.context.themeManager.theme } } componentDidMount() { this.context.themeManager.subscribe(() => this.setState({ theme: this.context.themeManager.theme // 核心代码 保证theme的更新 })) } render() { return <Button theme={this.state.theme} />; // 核心代码 } }
OK,回头看看咱们都干了些什么:
context
传递 theme
值,而是传递一个themeManager
注入对象,这个对象的特色是内置了状态更新和消息通知的功能。ThemedButton
订阅theme
的变化,而且利用setState
做为回调函数,保证theme
值的及时更新。从而完美绕开了context
的传递问题。其实,它一样符合咱们第一个解决方案:经过context
传递的对象,只被接受一次,而且后续都没有更新(都是同一个themeManager
对象,更新是经过themeManager
内部的自我管理实现的。)
讲完基本用法,接着聊聊context
在16.x版本以后的API。
先说一个好消息!使用新API后
每当 Provider(提供者) 的 value 属性发生变化时,全部做为 Provider(提供者) 后代的 consumer(使用者) 组件 都将从新渲染。 从Provider 到其后代使用者的传播不受 shouldComponentUpdate 方法的约束,所以即便祖先组件退出更新,也会更新 consumer(使用者)
换句话说 若是使用context
的新API,第三节能够跳过不看。(因此我把那一段写前面去了)
在传统版本,使用getChildContext
和childContextTypes
来使用context,而在16.x版本以后,前面的例子能够改写成这样:
首先使用createContext
建立一个context,该方法返回一个对象,包含Provider
(生产者)和Consumer
(消费者)两个组件:
const themeContext = React.createContext('light'); // 这里light是默认值 后续使用时能够改变
使用Provider
组件,指定context
须要做用的组件树范围
class App extends React.Component { render() { // 使用一个 Provider 来将当前的 theme 传递给如下的组件树。 // 不管多深,任何组件都能读取这个值。 // 在这个例子中,咱们将 “dark” 做为当前的值传递下去。 return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // 中间的组件不再必指明往下传递 theme 了。 function Toolbar(props) { return ( <ThemedButton /> ); }
后代组件根据须要,指定contextType
须要做用的组件树范围
class ThemedButton extends React.Component { // 指定 contextType 读取当前的 theme context。 // React 会往上找到最近的 theme Provider,而后使用它的值。 // 在这个例子中,当前的 theme 值为 “dark”。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; } } // 除了写static contextType = ThemeContext 也能够这样写: ThemedButton.contextType = ThemeContext;
固然,也能够经过Consumer
组件指定消费者
class ThemedButton extends React.Component { static contextType = ThemeContext; render() { // Consumer的children必须是一个函数,传递的等于组件树中层这个 context 最接近的 Provider 的对应属性 <ThemeContext.Consumer> { theme =><Button theme={theme} />; // 核心代码 } </ThemeContext.Consumer> } }
这两种方式的主要区别是若是须要传递多个可能同名的context
时(例如这个例子中Toolbar
组件也经过context传递一个theme
属性,而ThemedButton
须要的是从APP
来的theme),只能用Consumer
来写
对于context
的使用,须要注意的主要是如下2点:
context
,由于react
重视函数式编程,讲究复用,而使用了context
的组件,复用性大大下降react
,尤为要注意context
在本身的可控范围内,其实最大的问题也就是前面说的SUC
的问题context
的值变动时,Consumer
会受到相应的通知,所以要注意某些隐含非预期的变化,例如:// bad 示例, 由于每次render时{something: 'something'}都指向一个新对象(引用类型的值是老问题,不赘述了) class App extends React.Component { render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } } // good 示例 使用固定的变量存储值 固然能够选择除了state之外的其余变量 class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'something'}, }; } render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } }
顺便提一下react-router
其实也用了Context
的原理。<Router />
、<Link />
以及<Route />
这些组件之间共享一个router, 才能完成完成复杂的路由操做。有兴趣的能够自行查阅源码。(疯狂偷懒)
本文主要介绍了context
在react
的经常使用场景,以及在新旧API模式下的使用方法,着重介绍了shouldComponent
的处理方案。
-----惯例偷懒分割线-----
若是以为写得很差/有错误/表述不明确,都欢迎指出
若是有帮助,欢迎点赞和收藏,转载请征得赞成后著明出处。若是有问题也欢迎私信交流,主页有邮箱地址
若是以为做者很辛苦,也欢迎打赏~