重拾React: Context

栏目: IOS · Android · 发布时间: 5年前

内容简介:首先欢迎大家关注我的对于React开发者而言,Context应该是一个不陌生的概念,但是在16.3之前,React官方一直不推荐使用,并声称该特性属于实验性质的API,可能会从之后的版本中移除。但是在实践中非常多的第三方库都基于该特性,例如:react-redux、mobx-react。

前言

首先欢迎大家关注我的 Github博客 ,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励,希望大家多多关注呀!好久已经没写React,发现连Context都发生了变化,忽然有一种村里刚通上的网的感觉,可能文章所提及的知识点已经算是过时了,仅仅算作是自己的学习体验吧,

Context

对于React开发者而言,Context应该是一个不陌生的概念,但是在16.3之前,React官方一直不推荐使用,并声称该特性属于实验性质的API,可能会从之后的版本中移除。但是在实践中非常多的第三方库都基于该特性,例如:react-redux、mobx-react。

重拾React: Context

如上面的组件树中,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 (
            <button style={{background: this.context.color}}>
                {this.props.children}
            </button>
        );
    }
}

class Message extends React.Component {
    render() {
        return (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        );
    }
}

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

    getChildContext() {
        return {color: "red"};
    }

    render() {
        const children = this.props.messages.map((message) =>
            <Message text={message.text} />
        );
        return <div>{children}</div>;
    }
}

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

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

    _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 (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        );
    }
}

你会发现即使你在 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 (
            <button style={{background: this.context.theme.color}}>
                {this.props.children}
            </button>
        );
    }
}

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) =>
            <Message text={message.text} />
        );
        return (
            <div>
                <div>{children}</div>
                <button onClick={this._changeColor}>Change Color</button>
            </div>
        );
    }

    _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(
            <ThemeContext.Consumer>
                {({color}) => {
                    return (
                        <button style={{background: color}}>
                            {this.props.children}
                        </button>
                    );
                }}
            </ThemeContext.Consumer>
        );
    }
}

class Message extends React.PureComponent {
    render() {
        return (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        );
    }
}

class MessageList extends React.Component {

    state = {
        theme: { color: "red" }
    };

    render() {
        return (
            <ThemeContext.Provider value={this.state.theme}>
                <div>
                    {this.props.messages.map((message) => <Message text={message.text}/>)}
                    <button onClick={this._changeColor}>Change Color</button>
                </div>
            </ThemeContext.Provider>
        )
    }

    _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的滥用,否则会造成组件间依赖过于复杂。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spark技术内幕

Spark技术内幕

张安站 / 机械工业出版社 / 2015-9-1

Spark是不断壮大的大数据分析解决方案家族中备受关注的新增成员。它不仅为分布式数据集的处理提供一个有效框架,而且以高效的方式处理分布式数据集。它支持实时处理、流处理和批处理,提供了AllinOne的统一解决方案,使得Spark极具竞争力。 本书以源码为基础,深入分析Spark内核的设计理念和架构实现,系统讲解各个核心模块的实现,为性能调优、二次开发和系统运维提供理论支持;本文最后以项目实战......一起来看看 《Spark技术内幕》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码