超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

React - Context

注意,本文仅适用于 React16 以下的版本

Context 是一个实验性的 Api,和 props 一样用于组件之间的数据传递,但是这个功能却很少使用,甚至不为人知。

为何使用 Context

在使用 React 开发的时候,通常通过改变 state 和 传递 props 对组件进行控制,特别是通过 props 在组件间的数据流传输,可以很容易的推断组件的状态。首先看一下常用的组件间 props 传递的例子:

const ParentComponent = () => {
const colorTheme = "green";

return (
<MiddleChildComponent color={colorTheme} />
)
}

const MiddleChildComponent = ({color, ...props}) => {
// doSth(props)
return (<LeafChildComponent color={color} />);
}

const LeafChildComponent = ({color}) => {
// doSth(props)
return <div style={ {background: color} }>LeafChildComponent</div>;
}

这个例子是 React 经典的单向数据传递的结构,数据的流向是:

ParentComponent
--(props)--> MiddleChildComponent
--(props)--> LeafChildComponent

React 的这个结构也使得代码非常容易进行调试和维护,当代码出现问题的时候,只需要沿着这条路径进行追踪即可。

但是,当应用变大的时候,这个中间层次可能会变多,这样当传递某个特殊的 prop 的时候,需要穿越非常多的中间组件层级,而这些中间的组件并不会用到,设置根本不知道这个 prop 的存在,这样写起来无疑非常的繁琐,在这种情况下,可以考虑使用 Context Api ,使用 Context 可以不必再中间组件传递 props ,而能让后代获取到数据:

使用 Context

使用 Context 需要增加以下属性和方法:

  • 在父组件中定义方法 getChildContext ,方法返回子组件接收的 conext 对象。
  • 在父组件中定义属性 childContextType , 该属性指定了 getChildContext 方法返回的对象的数据类型 。
  • 在想要获取 context 的子组件定义属性 contextTypes 即可获取 context ,如果未定义该属性,则 context 是一个空对象。
  • 在子组件使用 this.context 获取 Context ,如果是无状态的 SFC 组件,则第二个参数传入 Context

使用 Context 重新改写上面的例子:

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

// 定义 context
getChildContext() {
return { color: 'green' }
}

render() {
return (
<MiddleChildComponent/>
)
}
}

// 未指定 contextTypes ,context 为空
const MiddleChildComponent = (props, context) => {
console.log("MiddleChildComponent context is: ");
console.log(context);
return (<LeafChildComponent />);
}

const LeafChildComponent = (props, context) => {
console.log("LeafChildComponent context is: ");
console.log(context);
return <div style={ {background: context.color} }>LeafChildComponent</div>;
}

// 为要获取 context 的组件指定属性 contextTypes
LeafChildComponent.contextTypes = {
color: React.PropTypes.string
}

ReactDOM.render(
<ParentComponent />,
document.getElementById('app')
)

// MiddleChildComponent context is:
// Object {}
// LeafChildComponent context is:
// Object {color: "green"}

props 或者 state 改变的时候,父组件就会调用 getChildContext 方法更新 context

组件的生命周期函数

如果为一个组件定义了 contentTypes 属性获取父组件的 contentTypes, 则以下几个生命周期函数会被传入一个额外的参数,即 context 对象:

  • constructor(props, context)
  • componentWillReceiveProps(nextProps, nextContext)
  • shouldComponentUpdate(nextProps, nextState, nextContext)
  • componentWillUpdate(nextProps, nextState, nextContext)
  • componentDidUpdate(prevProps, prevState, prevContext)

Context 的缺陷

事实上,绝大多数的应用都用不到 context ,使用 context 有以下问题:

这是一个实验性的 api

这是一个实验性的 api 这就意味着这个 api 未来可能发生变动,甚至会被删除,将进一步影响到使用这个 api 的应用和组件,因此最好避免使用它。

和特定的父组件耦合

如果一个组件使用了 context , 这就意味着这个组合使用的时候就必须和一个能提供所需 context 的父组件耦合在一起,这样就限制了组件的应用范围,组件很难被复用。

context 更新被阻断

当父组件的 state 或者 pros 变化的时候,父组件的 getChildContext 会被调用更新 context,同时父组件会更新其自身和并引发其子组件的更新,而子组件接收到的 context 也会被改变,这一点到不会引起问题,在某些情况下子组件可以依赖于 context 更新自身。

但是当中间组件使用了 shouldComponentUpdate 禁止更新,这样则子组件不会被更新,子组件则不会被渲染,此时父组件更新的值无法被应用,看下面代码:

父组件 ParentComponent 加载完成后修改 statecontext

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

state = {
color: 'green'
}

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

componentDidMount() {
this.setState({color: 'red'});
}

render() {
return (
<MiddleChildComponent/>
)
}
}

class MiddleChildComponent extends React.Component {
render() {
return (
<LeafChildComponent />
)
}
}

const LeafChildComponent = (props, context) => {
console.log("LeafChildComponent context is: ");
console.log(context);
return <div style={ {background: context.color} }>LeafChildComponent</div>;
}

// 为要获取 context 的组件指定属性 contextTypes
LeafChildComponent.contextTypes = {
color: React.PropTypes.string
}

ReactDOM.render(
<ParentComponent />,
document.getElementById('app')
)

// LeafChildComponent context is:
// Object {color: "green"}

// LeafChildComponent context is:
// Object {color: "red"}

从上面的 log 可以看到 context 是正确更新的,这次设置中间组件禁止更新:

class MiddleChildComponent extends React.Component {
shouldComponentUpdate() {
return false;
}

render() {
return (
<LeafChildComponent />
)
}
}

// LeafChildComponent context is:
// Object {color: "green"}

从执行结果可以看到,第二次的 context 更新被中间组件阻断,依赖 context 进行组件的更新是有风险的。

难以维护

当维护别人的组件的时候,如果一些组件使用了 context 这样就非常的困难了,需要从大量的组件中找到要使用的组件,当项目比较大的时候无疑是非常困难的。而如果采用了 props 就可以沿着调用的方向向上找,很容易就找到定义的位置。

应用场景

Dan Abramov 设计了一些明智的规则关于使用 context:

function shouldIUseReactContextFeature() {
// 类库作者 或者 向下传递很深的层级
if (amIALibraryAuthor() && doINeedToPassSomethingDownDeeply()) {
// 很乐意处理 api 修改和 bug
// A custom <Option> component might want to talk to its <Select>.
// This is OK but note that context is experimental API and doesn't update
// correctly in some cases so you might want to roll your own subscriptions.
return amIFineWith(API_CHANGES && BUGGY_UPDATES);
}

// 主题 或 本地化场景
if (myUseCase === 'theming' || myUseCase === 'localization') {
// Context 可以用以存放全局变量或者很少改变的内容
// 如果坚持使用的话可以用在 HOC 组件中,而不是直接使用 this.context ,这样当接口改变
// 的时候可以很容易的只修改一个地方
// In apps, context can be used for "global" variables that rarely change.
// If you insist on using it, provide a higher order component.
// This way when we change the API, you will only need to update one place.
return iPromiseToWriteHOCInsteadOfUsingItDirectly();
}

if (libraryAsksMeToUseContext()) {
// Ask them to provide a higher order component!
throw new Error('File an issue with this library.');
}

// Good luck.
return yolo();
}

上面基本已经说明了应用场景:类库作者、全局级别的信息(主题、语言、环境、用户信息等),并且在使用的时候要考虑 api 在后面版本发生修改等问题,当然也有使用 context 非常出色的类库: react-routerreact-redux

本文最后自定义一个路由管理组件 Router 作为练习。Router 组件包含两个子组件 RouterRouteRouter 组建包括整个应用,Route` 用于匹配路径到组件。

首先定义 Router ,它提供了 Route 组件需要的几个操作,

import { Component, PropTypes } from 'react';

class Router extends Component {
getChildContext() {
const register(url) {
console.log('registered route!', url)
}
const location = location.href;
return {
router: {
register,
location
}
}
}

render() {
return <div>{this.props.children}</div>
}
}

Router.childContextTypes = {
router: PropTypes.object.isRequired,
}

然后 Route 组件需要使用 contextrouter

class Route extends Component {
componentWillMount() {
this.context.router.register(this.props.path)
}
match() {
return this.context.router.location === this.props.path;
}
render() {
console.log(`I am the route for ${this.props.path}`)
return <div>{this.match()? this.props.children: null}</p>
}
}
Route.contextTypes = {
router: PropTypes.object.isRequired,
}

这样就可以在 app 入口开始使用 Router 组件:

const App = () => (
<div>
<Router>
<div>
<Route path="/foo" />
<Route path="/bar" />
<div>
<Route path="/baz" />
</div>
</div>
</Router>
</div>
)

使用这种方式的优点在于,只要保证 Route 组件被嵌套在 Router 组件之内,Route 可以放在任意的位置,任意嵌套,都能正常使用。

参考