资讯专栏INFORMATION COLUMN

重拾React: Context

曹金海 / 2922人阅读

前言

  首先欢迎大家关注我的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 (
            
        );
    }
}

class Message extends React.Component {
    render() {
        return (
            
{this.props.text}
); } } class MessageList extends React.Component { static childContextTypes = { color: PropTypes.string }; getChildContext() { return {color: "red"}; } render() { const children = this.props.messages.map((message) => ); return
{children}
; } }

  我们可以看到MessageList通过函数getChildContext显式声明提供color属性,并且通过静态属性childContextTypes声明了该属性的类型。而Button通过静态属性contextTypes声明了要使用属性的类型,二者通过协商的方式约定了跨层级传递属性的信息。Context确实非常方便的解决了跨层级传递属性的情况,但是为什么官方却不推荐使用呢?

  首先Context的使用是与React可复用组件的逻辑背道而驰的,在React的思维中,所有组件应该具有复用的特性,但是正是因为Context的引入,组件复用的使用变得严格起来。就以上面的代码为例,如果想要复用Button组件,必须在上层组件中含有一个可以提供String类型的colorContext,所以复用要求变得严格起来。并且更重要的是,当你尝试修改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) =>
            
        );
        return (
            
{children}
); } _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 (
            
{this.props.text}
); } }

  你会发现即使你在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 (
            
        );
    }
}

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) =>
            
        );
        return (
            
{children}
); } _changeColor = () => { const colors = ["red", "green", "blue"]; const index = (colors.indexOf(this.theme.color) + 1) % 3; this.theme.setColor(colors[index]); } }

  在上面的例子中我们创造了一个Theme类用来管理样式,然后通过ContextTheme的实例向下传递,在Button中获取到该实例并且订阅样式变化,在样式变化时调用forceUpdate强制刷新达到刷新界面的目的。当然上面的例子只是一个雏形,具体使用时还需要考虑到其他的方面内容,例如在组件销毁时需要取消监听等方面。

  回顾一下之前版本的Context,配置起来还是比较麻烦的,尤其还需要在对应的两个组件中分别使用childContextTypescontextTypes的声明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

  因此我们能得出静态属性childContextTypescontextTypes只能提供开发的辅助性作用,对实际的context取值并不能起到约束性的作用,即使这样我们也不得不重复体力劳动,一遍遍的声明childContextTypescontextTypes属性。

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(
            
                {({color}) => {
                    return (
                        
                    );
                }}
            
        );
    }
}

class Message extends React.PureComponent {
    render() {
        return (
            
{this.props.text}
); } } class MessageList extends React.Component { state = { theme: { color: "red" } }; render() { return (
{this.props.messages.map((message) => )}
) } _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才会使用,因此即使你给Providervalue传入undefined值时,Consumer也不会使用默认值。

  新版的Context API相比于之前的Context API更符合React的思想,并且能解决componentShouldUpdate的带来的问题。与此同时你的项目需要增加专门的文件来创建Context。在 React v17 中,可能就会删除对老版 Context API 的支持,所以还是需要尽快升级。最后讲了这么多,但是在项目中还是要尽量避免Context的滥用,否则会造成组件间依赖过于复杂。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101146.html

相关文章

  • 重拾React: React 16.0

    摘要:然而之前的相当于从最顶层的组件开始,自顶向下递归调用,不会被中断,这样就会持续占用浏览器主线程。众所周知,是单线程运行,长时间占用主线程会阻塞其他类似于样式计算布局绘制等运算,从而出现掉帧的情况。 前言   首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励,希望大家多多关注呀!从今年年初离开React开发岗,...

    henry14 评论0 收藏0
  • 重拾JSX

    摘要:语法糖是一种的语法拓展,可以使用它来进行的展示我们一般会在组件的方法里使用进行布局和事件绑定的核心机制之一就是可以创建虚拟的元素,利用虚拟来减少对实际的操作从而提升性能,正是为了虚拟而存在的语法糖我们在平时的组件编写中,通常都这么写然而代码 React.createElement语法糖 JSX是一种JavaScript的语法拓展,可以使用它来进行UI的展示: const element...

    adam1q84 评论0 收藏0
  • 重拾-MyBatis-配置文件解析

    摘要:前言我们知道在使用时,我们需要通过去创建实例,譬如为的配置文件那么我们看下方法的具体实现创建实例并执行解析主要通过执行对配置文件的解析,具体实现如下文配置文件解析解析标签解析标签解析别名标签解析插件标签解析标签解析标签解析标签从的方法实现我 前言 我们知道在使用 Mybatis 时,我们需要通过 SqlSessionFactoryBuild 去创建 SqlSessionFactory ...

    王晗 评论0 收藏0
  • 重拾-Spring-IOC

    摘要:为何重拾使用了多年,但是对其底层的一些实现还是一知半解,一些概念比较模糊故决定重新拾起,加深对的认识。小结是在完成创建后对其进行后置处理的接口是在完成实例化对其进行的后置处理接口是框架底层的核心接口,其提供了创建,获取等核心功能。 为何重拾 使用了 Spring 多年,但是对其底层的一些实现还是一知半解,一些概念比较模糊;故决定重新拾起,加深对 Spring 的认识。 重拾计划 spr...

    GraphQuery 评论0 收藏0
  • 重拾golang - go目录结构说明

    摘要:目录结构说明集多编程范式之大成者,使开发者能够快速的开发测试部署程序,支持全平台静态编译。上目录位置主要目录包含如下图,分别进行说明文件夹存放检查器的辅助文件。工作区有个子目录目录目录和目录。目录用于以代码包的形式组织并保存源码文件。 go 目录结构说明   golang集多编程范式之大成者,使开发者能够快速的开发、测试、部署程序,支持全平台静态编译。go具有优秀的依赖管理,高效的运行...

    zhisheng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<