乐趣区

浅谈react-context

前言

好久不见!(两个多月没更新内容,惭愧了三分钟)。接下来的文章主要是开始对 react 的内容做一些整理(疯狂立 Flag)。本文的对象是Context.

正文

1. 为什么需要使用 Context

在 React 中,数据传递一般使用 props 传递数据,维持单向数据流,这样可以让组件之间的关系变得简单且可预测,但是单项数据流在某些场景中并不适用,看一个官方给出的例子:
有三个组件 APPToolbarThemedButton,关系如图:( 为了方便大家理解(偷懒),这个例子我会全文通用。

APP存放着主题相关的参数theme,需要传递组件ThemedButton, 如果考虑使用props,那么代码就长这样:

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 作为中介传递。不妨再引申思考一下:

  1. 如果 ThemedButton 并非末级子节点,那么参数必须继续向下传递
  2. 如果 App 中,还有除了 <ThemedButton> 以外的组件,也需要 theme 参数,那么也必须按照这种形式逐层传递

那么数据结构图大概如图所示:

结构图 placeholder:层层传递

显然,这样做太!繁!琐!了!

接下来,就要介绍今天的主角 –Context

2. Context 的用法介绍

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

上面是官方对于 context 的介绍,简单来说,就是可以把 context 当做是 特定一个组件树内 共享的 store,用来做数据传递。
为什么这里要加粗强调组件树呢?因为它是基于树形结构共享的数据:在某个节点开启提供 context 后,所有后代节点 compoent 都可以获取到共享的数据。

语言描述略显抽象,直接上代码:

1. 基本使用

以下介绍的是在 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 中解放出来了。

在代码中我们提到了 providerconsumer, 这里简单解释下:
context使用的生产者 provider– 消费者consumer 模式,

  • 把提供 context 的叫做provider,比如例子中的APP,
  • 把使用 context 的称为consumer, 对应例子中的ThemedButton

2. 更新 context

如果我们在 APP 组件提供了一个切换主题的按钮,那就需要 context 能够更新并且通知到相应的 consumer
由于 context 本身提供了相关功能:

  1. getChildContext方法在每次 stateprops改变时会被调用;
  2. 一旦 provider 改变了 context,所有的后代组件中的consumer 都会重新渲染。

所以通常的方式是:将 context 的数据保存在 Providestate属性中,每次通过 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 />;}
}

但是官方文档同时提到了这种方法是有隐患的,下一节进行详细解析。

3. 当 context 遇到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 的更新!

  1. 第一种思路 :首先,我们先看看问题的根源之一,是context 更新之后 ,后代节点无法及时获取到更新, 那么如果 context 不发生更,那就不存在这个问题了.【我个人觉得这个思路有点类似于,解决不了问题,可以考虑解决提出问题的人】,也就意味着:

    • 设定为不可变对象immutable
    • 后代组件应该 仅在 constructor 函数中 获取一次context
  2. 第二种思路 ,我们不在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, 回头看看我们都干了些什么:

  1. 我们现在不再利用 context 传递 theme值,而是传递一个 themeManager 注入对象,这个对象的特点是 内置了状态更新和消息通知的功能
  2. 消费组件 ThemedButton 订阅 theme 的变化,并且利用 setState 作为回调函数,保证 theme 值的及时更新。

从而完美绕开了 context 的传递问题。其实,它同样符合我们第一个解决方案:通过 context 传递的对象,只被接受一次,并且后续都没有更新 (都是同一个themeManager 对象,更新是通过 themeManager 内部的自我管理实现的。)

4. 16.x 后的新 API

讲完基本用法,接着聊聊 context 在 16.x 版本之后的 API。
先说一个好消息!使用新 API 后

每当 Provider(提供者) 的 value 属性发生变化时,所有作为 Provider(提供者) 后代的 consumer(使用者) 组件 都将重新渲染。从 Provider 到其后代使用者的传播不受 shouldComponentUpdate 方法的约束,因此即使祖先组件退出更新,也会更新 consumer(使用者)

换句话说 如果使用 context 的新 API,第三节可以跳过不看。(所以我把那一段写前面去了)

在传统版本,使用 getChildContextchildContextTypes来使用 context, 而在 16.x 版本之后,前面的例子可以改写成这样:

  1. 首先使用 createContext 创建一个 context,该方法返回一个对象, 包含Provider(生产者)和Consumer(消费者)两个组件:

    const themeContext = React.createContext('light'); // 这里 light 是默认值 后续使用时可以改变
  2. 使用 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 />);
    }
  3. 后代组件根据需要,指定 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 来写

5. 注意事项和其他

对于 context 的使用,需要注意的主要是以下 2 点:

  1. 减少不必要使用 context,因为react 重视函数式编程,讲究复用,而使用了 context 的组件,复用性大大降低
  2. 传统版本的 react, 尤其要注意context 在自己的可控范围内,其实最大的问题也就是前面说的 SUC 的问题
  3. 前面说到 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,才能完成完成复杂的路由操作。有兴趣的可以自行查阅源码。(疯狂偷懒)

总结

本文主要介绍了 contextreact的常用场景,以及在新旧 API 模式下的使用方法,着重介绍了 shouldComponent 的处理方案。

—– 惯例偷懒分割线 —–
如果觉得写得不好 / 有错误 / 表述不明确,都欢迎指出
如果有帮助,欢迎点赞和收藏,转载请征得同意后著明出处。如果有问题也欢迎私信交流,主页有邮箱地址
如果觉得作者很辛苦,也欢迎打赏~

退出移动版