我在从零开始的react入门教程(八),redux起源与基础用法一文中,介绍了redux
的前辈Flux
,以及redux
关于单项数据更新的基本用法。我们在前文提到,相对Flux
支持多个store
,redux
推荐唯一数据源,也就是使用一个全局Store
去掌管所有数据。数据源虽然统一了,但我们要使用Store
还是得把Store
引入到需要的组件中,比如上文中的Counter
组件与Summary
组件,毕竟使用dispatch
或者监听Store
变化都离不开这个数据源,但这就造就了两个问题。
问题一,假设我们有多个组件都依赖了Store
数据,组件分布在不同文件夹,或者说我们使用的三方库也依赖了此数据,用一处就得引一次,文件路径的相对关系都是一个不小的麻烦。
问题二,可能有同学就想到,哎,react不是有个概念叫状态提升吗,大不了我在顶层组件引用一次,通过props
进行数据传递,但这样就会造成多个组件其实并不需要这份数据,但为了子孙组件能顺利访问数据,都成了数据传递的搬运工。
针对上面两个问题,我们其实可以通过Context
得以解决,Context
顾名思义就是上下文。就像在一个作用域内我们提前声明了一个变量,后续代码不需要再做引用操作,你都能直接访问它,Context
的作用也是如此。
我在整理Context
资料的时候发现了一个问题,由于react
版本原因,react
对于Context
的解释也是存在历史变迁的。作为一个初学者,如果你在百度想搜Context
用法然后发现了不同的介绍,估计你也会纳闷我到底应该用哪种(或者对于直接上手react-redux的同学可能根本没了解过原生Context的用法),这里我先做个简单的总结,在react
版本16.X之前,Context
的使用依赖childContextTypes
对象,然后手动定义Provider
组件,比如在《深入浅出react和Redux》一书中,代码例子的react版本还是15.4.1,所以书中介绍的自然是前面提到的做法。而对于现在的版本比如官方文档中,Context
的使用已经不需要手动定义Provider
组件了,而是createContext
方法手动创建,用法上会人性化很多。
本文还是会站在不同的两个版本,去介绍它们的用法,以达到解决文章开头关于Store
引用与传递的问题,当然,如果你已经确定了当前项目的react版本,你可以自由选择对应的版本文档了解其用法。
如果可以,我还是希望有缘看到这篇文章的人能跟着手敲代码,感受其具体的用法,那么本文开始。
说在前面,下面的代码仍然基于上一篇文章的例子修改,当然如果没有代码,我尽可能将使用上的细节描述清楚(当然我还是推荐跟着例子来)。如果大家有简单了解过Context
,脑海里一定对Provider
的单词有所印象,不过对于老版本而言,我们并不能直接引用并使用它,而是需要自己创建,确实非常尴尬。
我们现在src目录下新建一个Provider.js
的文件,里面的代码为:
import {Component} from 'react';
import PropTypes from 'prop-types';
class Provider extends Component {
getChildContext() {
// 我们会通过store字段将全局store传递进来
return {
store: this.props.store
};
}
// 渲染Provoder所包裹的子组件内容
render() {
return this.props.children;
}
}
Provider.propTypes = {
store: PropTypes.object.isRequired
}
Provider.childContextTypes = {
store: PropTypes.object
};
export default Provider;
这段代码有几点需要拧出来说,第一个是关于PropTypes
,写过react的同学都知道这是做组件属性的类型检查,比如我一个组件哪些属性是必须提供,哪些是字符串等等。这个东西呢其实也存在一个历史问题....早期版本的react,是可以直接通过引用拿到此对象然后使用,比如:
import { PropTypes } from 'react';
但是在react 15.5之后,此属性被react官方废弃掉了,如果你是版本比较高的react,像上面这样引用会告诉你PropTypes
是undefied
并报错,比如我参考的《深入浅出react和Redux》一书中都是这么用的,因为作者例子的react版本也比较低(15.4.1),而我在写demo的react版本已经是16了,自然用不了,不过也没有关系,咱们可以通过如下方式引用PropTypes
:
import PropTypes from 'prop-types';
prop-types
是一个独立的三方库,因此我们需要提前安装这个包,比如执行命令yarn add prop-types
,若你是npm请执行npm i prop-types
,这里就不多介绍了,关于prop-types
后续也可能会专门写一篇用法的文章。
回到上面的代码中,Provider
组件定义的内容其实非常简单,一个getChildContext
方法,用于创建子组件的上下文,而上下文中包含的东西其实也就是我们需要使用的store
数据,this.props.store
怎么来下面的代码会交代。除此之外还有一个render方法,用于渲染Provider
包裹的子组件。关于this.props.children
这里做个简单补充,比如我们有一个父组件A与一个子组件B,A包裹B,如下:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
function A(props) {
console.log(props);
return <div>我是父组件{props.children}</div>
}
function B() {
return <div>我是子组件</div>
}
ReactDOM.render(
<A><B/></A>,
document.getElementById('root')
);
可以看到我们使用了A包裹了B,在A组件的返回中,我们通过{props.children}
成功拿到了包裹的B组件,并将其渲染了出来,通过控制台输出也看的很明显,这里的chindren
属性其实就是组件A所包含的组件内容。
我们再过分点,直接修改为如下代码:
ReactDOM.render(
<A>
{
<div>
<div>1</div>
<div>2</div>
</div>
}
</A>,
document.getElementById('root')
);
再看控制台,你会发现通过children
属性,我们先访问到了包裹的最外层的div,然后此div的children
又是一个数组,因为它又包含了两个div,继续再通过children
属性,我们就可以找到数组第一个元素的孩子是一个数字1,这就是react中children
的作用,在实际开发中,我们也常会利用此属性达到组件父子组件嵌套的目的。
OK,题外话说完了,再回到上述代码,注意如下这段代码:
Provider.childContextTypes = {
store: PropTypes.object
};
这段代码是必须提供的,不然直接报错,它的类型定义与getChildContext
方法中提供的类型相对应,它用于告诉react我现在为子组件提供了一个上下文,上下文中包含的数据有哪些,每个属性是什么类型,关于Provider.js
先说到这里。
在上一篇文章的例子中,我们通过index.js
文件最终渲染了所有组件,这里我们需要做些修改,具体如下:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import store from './Store.js';
import Provider from './Provider.js';
import Counter from './Counter.js';
import Summary from './Summary.js';
class ControlPanel extends Component {
render() {
return (
<Provider store={store}>
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
</Provider>
);
}
}
ReactDOM.render(
<ControlPanel />,
document.getElementById('root')
);
我们在此文件中引用了前面定义的Provider
组件,同时也引用了全局的Store
,然后通过Provider
组件将上篇文章中需要渲染的组件进行了包裹,同时通过store
字段将引用过来的store
作为props传递了下去,这里就对应了Provider.js
中getChildContext
方法this.props.store
的来源。
上述的修改其实很好理解,我们将Provider
作为顶层组件,为需要渲染的所有组件提供了一个共有的上下文,而这个上下文中存在一个store
属性,也就是全局的Store
,现在子组件们不需要再分别引用Store.js
文件了,但这些子组件还需要做一些改变才能支持访问上下文。
以Counter
组件为例,这里我们说下需要修改的几个点:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as Actions from './Actions.js';
class Counter extends Component {
constructor(props,context) {
super(props,context);
// 初始化组件的state
this.state = this.getOwnState();
}
getOwnState = () => {
// 这里的this.props.caption其实就是前面说的First Second
return {
// 这里可以拿到当前的Store数据,并根据key取到对应的初始值
value: this.context.store.getState()[this.props.caption]
};
}
onIncrement = () => {
// Actions.increment返回的其实是一个action对象,注意这个函数其实只传递了一个参数,也就是上面提到的First Second类型
this.context.store.dispatch(Actions.increment(this.props.caption));
}
onDecrement = () => {
this.context.store.dispatch(Actions.decrement(this.props.caption));
}
// 用于更新state
onChange = () => {
this.setState(this.getOwnState());
}
shouldComponentUpdate(nextProps, nextState) {
// 如果state的value变了,通知组件更新
return nextState.value !== this.state.value;
}
componentDidMount() {
// 监听Store变化,Store变了我们就让组件的state也跟着变
this.context.store.subscribe(this.onChange);
}
componentWillUnmount() {
this.context.store.unsubscribe(this.onChange);
}
render() {
const { value } = this.state;
const { caption } = this.props;
return (
<div>
<button onClick={this.onIncrement}>+</button>
<button onClick={this.onDecrement}>-</button>
<span>{caption} count: {value}</span>
</div>
);
}
}
// 这里必须定义,不然访问不到Context
Counter.contextTypes = {
store: PropTypes.object
}
export default Counter;
第一点就是我们同样引入了PropTypes
,因为在代码最下面,我们必须定义contextTypes
的类型,这里与Provider.js
中的childContextTypes
定义其实是对应的,上下文在创建的时候定义了,子组件在引用上下文时同样得做一个定义声明。
第二点,在constructor
中我们知道super
方法用于子组件在初始化时继承父组件传递的属性,而这里我们得额外添加一个context
,表示将上下文传递进来。
第三点,之前在Counter
中我们直接引入了Store.js
,因此可以直接访问store
的数据以及API方法,但此时我们是通过上下文访问,因此需要对之前所有使用到store
的前面添加上this.context
,具体可参照上述代码。同理,我们将Summary
组件中也做上述三点修改,然后执行yarn start
运行项目,你会发现非常完美,项目成功跑起来了。
那么到这里,我们通过旧版的Context
做法取代了传统Store
引用的做法,达到了只在index.js
一处引用统一管理,并可在所有子组件中访问此上下文的目的。
其实对前面旧版的修改写下来,你会发现这玩意还真不是那么好用,虽说不用每个组件引入Store
了吧,咱还得自己手写Provider
组件不说,每个用到store
的组件还得专门定义contextTypes
的类型,实属有点麻烦。没事,我们继续来看新版的Context
的用法。当然这次,至少咱们不用手写Provider
组件了。
在对于新版本Context
资料查阅中,我看到了一句对于Context
作用描述比较精准的话,那就是Context
能实现组件跨层级的数据传递。比如Props
传递一定是逐层的,这可能就会对一些不需要这部分数据的组件造成感染,那么我想越级传递,中间的组件不需要感知这部分数据的存在,Context
就是一个不错的渲染。当然回到上文,我们还是可以理解为Context
为相关联的组件提供了一个共有上下文,子可见后代也可见,那么就不需要子帮忙传递后代都可以拿着用。因此,除了应对全局Store
的数据传递之外,某些部分组件的数据越级传递(比如数据与Store无关,单纯几个层级关系组件之间需要做传递),以及部分子组件,后代组件都需要访问到父组件的部分数据,其实都可以使用此做法达到目的。
OK,新版Context
的几个核心概念为createContext
,Provider
与Consumer
,我们一个个说。
createContext
顾名思义,创建一个上下文也就是Context
对象,它的一般用法为:
const context = React.createContext();
而这个创建出来的context
对象中,又包含了Provider
与Consumer
两个组件,输出如下:
因此在使用时,其实也可以像下面这样直接获取到两个组件:
const {Provider, Consumer} = React.createContext();
createContext
可以接受一个参数defaultValue
,表示我在创建这个上下文时,就默认定义了一部分的共有的数据,但这个默认数据生效是有条件的,这里引用官方文档的描述:
createContext
创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的Provider
中读取到当前的 context 值。而如果当前组件所处的组件树中都没有匹配到Provider
是,这时候defaultValue
就会生效。
怎么理解呢?也就是说我们在父组件创建了一个上下文,但后代组件中只用了Consumer
组件,而没有使用Provider
对应提供数据,那这时候相当于处于保护措施,我们让defaultValue
生效,保证Consumer
能拿到默认的数据,免得组件渲染报错了,实属吃低保的行为了。关于这部分的例子,可以参阅React.createContext point of defaultValue?的问题回复,因为这部分知识又涉及到了hook
的useContext
,简单理解就是父组件中createContext
创建上下文,而在子组件中可以使用useContext
解析context
中的数据,这里我们先不细谈。
顾名思义,与旧版我们定义的Provider
作用大致相同,它用于包裹需要享有相同上下文的所有组件,以及为其提供上下文中共有的数据,但需要注意的是,这里的数据传递必须通过value
字段,比如:
<Provider value={/*需要传递的共享数据*/}>
/*被包裹的组件们*/
</Provider>
多个Provider
可以嵌套使用,但是里层的Provider
的value会覆盖掉外层的Provider
的value,因此Consumer
访问context注定是访问距离自己最近的Provider
。除此之外还有一点,当Provider
传递的value发生了变化时,Provider
内部的所有Consumer
组件都会被强制重新渲染,shouldComponentUpdate
这玩意都不会限制住它,目的是保证所有消费者组件永远同步感知最新的context变化。
如名称理解的那样,消费者,也就是消费(使用)Provider
传递下来数据的组件。正常情况下,Consumer
组件得嵌套在Provider
组件之下,但如果如上面所说我们没用Provider
组件只用了Consumer
组件,那么Consumer
组件能访问的上下文就是在createContext
中定义的defaultValue
。
基本API都介绍了,我们来通过这种方式再来改写我们前面的例子。
首先,我们在src目录下新建一个Context.js
文件,代码如下:
import React from 'react';
const context = React.createContext();
export default context;
之后,在index.js
文件引入context,这里直接再贴上代码:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import store from './Store.js';
import Counter from './Counter.js';
import Summary from './Summary.js';
import context from './Context.js';
class ControlPanel extends Component {
render() {
return (
//我们使用了Provider包裹子组件,通过value传递store
<context.Provider value={store}>
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
</context.Provider>
);
}
}
ReactDOM.render(
<ControlPanel />,
document.getElementById('root')
);
同理,我们再次修改Counter
组件,还是直接上代码:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as Actions from './Actions.js';
import context from './Context.js';
// const context = React.createContext();
class Counter extends Component {
// static contextType = context;
constructor(props,context) {
super(props,context);
// 初始化组件的state
this.state = this.getOwnState();
}
getOwnState = () => {
// 这里的this.props.caption其实就是前面说的First Second
return {
// 这里可以拿到当前的Store数据,并根据key取到对应的初始值
value: this.context.getState()[this.props.caption]
};
}
onIncrement = () => {
// Actions.increment返回的其实是一个action对象,注意这个函数其实只传递了一个参数,也就是上面提到的First Second类型
this.context.dispatch(Actions.increment(this.props.caption));
}
onDecrement = () => {
this.context.dispatch(Actions.decrement(this.props.caption));
}
// 用于更新state
onChange = () => {
this.setState(this.getOwnState());
}
shouldComponentUpdate(nextProps, nextState) {
// 如果state的value变了,通知组件更新
return nextState.value !== this.state.value;
}
componentDidMount() {
// 监听Store变化,Store变了我们就让组件的state也跟着变
this.context.subscribe(this.onChange);
}
componentWillUnmount() {
this.context.unsubscribe(this.onChange);
}
render() {
const { value } = this.state;
const { caption } = this.props;
return (
<div>
<button onClick={this.onIncrement}>+</button>
<button onClick={this.onDecrement}>-</button>
<span>{caption} count: {value}</span>
</div>
);
}
}
Counter.contextType = context;
export default Counter;
因为我们需要在Counter
组件使用context
,因此也需要引入context
。之后,我们通过Counter.contextType = context;
为当前组件绑定context
对象,同理,在constructor
中还是得初始化context
,之后在组件任意地方,我们都可以通过this.context
访问到传递进来的store
,注意啊,这里的this.context
已经等同于store
本身了,所以代码中是this.context.subscribe
直接调用store
上的API。你可能有点不习惯,还是希望this.context.store
去访问,那就像如下方式这样传递,比如假设我们需要给Provider
传递多个值:
class ControlPanel extends Component {
render() {
const value = {
store,
name:1
};
return (
<context.Provider value={value}>
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
</context.Provider>
);
}
}
我们再去Counter
断点this,你就发现这就是你预期的样子了
其实可以发现,新版的context
在使用上与旧版还是有些类似的,在使用context
的地方同样得为组件做contextType
的定义以及context
的初始化,我们同理去修改掉Summary
中的代码,执行运行项目的命令,你会发现也能完美跑起来,那么到这里,我们又通过新版Context
的做法修改了例子。
当然到这里我们还没用到Consumer
,那么接下来我们再单独用一个例子,再次结合把Provider
与Consumer
用一用。接下来我们定义ABC三个组件,A嵌套B,B又嵌套C,直接修改index.js
中的代码:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import context from './Context.js';
class A extends Component {
render() {
const name = '听风是风';
return (
<context.Provider value={name}>
<div>{`我是A组件,我传递了${name}`}</div>
{/* 注意,这里我们并没有将name作为props传递下去 */}
<B />
</context.Provider>
)
}
}
function B() {
return (
<context.Consumer>
{
(name) => {
console.log(name);
return (
<div>
{`我是B组件,我接受了${name}`}
<C />
</div>
)
}
}
</context.Consumer>
)
}
function C() {
return (
<context.Consumer>
{
(name)=>{
return (
<div>
{`我是C组件,我接受了${name}`}
</div>
)
}
}
</context.Consumer>
)
}
ReactDOM.render(
<A />,
document.getElementById('root')
);
可以看到,在子组件需要使用context
的地方,我们通过context.Consumer
将其包裹,而context.Consumer
之间接受一个函数,此函数接受一个参数(参数随便你叫什么),此参数就是Provider
的映射,比如我们上面传递的是一个字符串,注意,只有一层花括号进行了包裹,所以函数形参name
直接就是所传递值的映射。
那假设我们传递了多个参数呢?还是一样,我们稍作修改,这里只贴上修改的部分,并在子组件函数中尝试打印:
class A extends Component {
render() {
const name = '听风是风';
const age = '28';
return (
<context.Provider value={{name,age}}>
<div>{`我是A组件,我传递了${name}`}</div>
{/* 注意,这里我们并没有将name作为props传递下去 */}
<B />
</context.Provider>
)
}
}
function B() {
return (
<context.Consumer>
{
// 参数其实可以随便你取名
(aaa) => {
console.log(aaa);
return (
<div>
{`我是B组件,我接受了${aaa.name}`}
<C />
</div>
)
}
}
</context.Consumer>
)
}
当然实际开发中,我们不会推荐这样传递多个参数,因为上述代码中value={{name,age}}
部分,代码每次执行{name,age}
可以理解为每次都是一个全新的对象,由于对象引用不同这会导致react
认为value
每次都在发生变化,从而引发子组件全部更新,推荐的做法是使用一个变量去声明一个对象包含这两个变量,比如:
// 这里只贴主要修改部分
const user = {
name:'听风是风',
age:28
}
return (
<context.Provider value={user}>
<div>{`我是A组件,我传递了${user.name}`}</div>
{/* 注意,这里我们并没有将name作为props传递下去 */}
<B />
</context.Provider>
)
<context.Consumer>
{
(user) => {
return (
<div>
{`我是B组件,我接受了${user.name}`}
<C />
</div>
)
}
}
</context.Consumer>
那么到这里,我们其实展示了两种在子组件中访问context
的方式,第一种是为组件绑定contextType
,第二种就是使用Consumer
,那么我们直接将C组件修改成如下的方式:
class C extends Component {
constructor(props, context) {
super(props, context)
}
render() {
return (
<div>
{`我是C组件,我接受了${this.context}`}
</div>
)
}
}
C.contextType = context;
可以看到我们没有借用Consumer
,而是借用组件contextType
绑定后,同样成功访问到了父组件传递的数据。
那么到这里,我们介绍了react
中新旧context
的基本用法,旧版context
需要自定义Provider
,并结合getChildContext
定义为子组件传递的数据。而新版context
在使用上相对友好了不少,我们可以通过createContext
创建一个context
实例,并可以直接使用Provider
提供数据,使用Consumer
消费数据。通过文中新旧例子对比,其实两者在使用上存在不少相同点。
在下一篇文章中,我们来了解react-redux
基本用法,其实本篇文章与上一篇文章属于react-redux
的铺垫篇,在了解了react原生的概念后,我想在理解三方封装时应该会容易很多,那么到这里本文结束。
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!