• 从零开始的react入门教程(七),react中的状态提升,我们为什么需要使用redux


    壹 ❀ 引

    在前面的文章中,我们了解到react中的数据由props与State构成,数据就像瀑布中的水自上而下流动,属于单向数据流。而这两者的区别也很简单,对于一个组件而言,如果说props是外部传递进来的属性,那么State便是组件内部自身提供的属性。当然这个组件又可以将自己的State与props作为props继续传递给自己的子级,比如下图:

    而对于props与State的通信,我们也在前文中提供了一些例子,但这些例子相对都比较简单,都是容易理解的父传子。但在实际开发中,组件之间的关系往往比我们学习时遇到的例子要复杂的多,高层级组件嵌套,兄弟组件通信,子传父等等都是在写组件时很常见的问题。其实说到这里,我想各位已经想到了redux的状态管理(vue中的vuex)。但本文并不会直接介绍redux,在介绍redux之前,我们还是需要了解react自身提供的状态管理做法,因为只有这样,我们才能明白为什么需要使用redux,以及react的状态提升的局限性在哪。那么本文开始。

    贰 ❀ react的状态提升

    react中的状态提升其实很好理解,由于react属于单向数据流,当有多个组件需要使用相同的数据时,比如兄弟组件相互感知数据变化,在react中我们一般推荐将这份数据提升到两兄弟的共同父组件中进行管理,这便是所谓的状态提升。

    让我们说的更直白点,因为瀑布的水(props)没办法横向流动(兄弟组件之间),所以我们将水源提到两兄弟的顶部,让它同时灌溉这两兄弟,从而满足了瀑布水自上而下的特性。

    这里我们直接引用react官方温度计的例子来了解这种做法的含义。

    假设现在我们有一个简单的温度计组件,输入一个温度,当大于等于100摄氏度时,文案提示水烧开了,反之提示水未烧开,直接上代码:

    function BoilingVerdict(props) {
        return props.celsius >= 100 ? <p>水烧开了。</p> : <p>水未烧开。</p>;
    }
    class Calculator extends React.Component {
        constructor(props) {
            super(props);
            this.handleChange = this.handleChange.bind(this);
            this.state = { temperature: 0 };
        }
    
        handleChange(e) {
            this.setState({ temperature: e.target.value });
        }
    
        render() {
            return (
                <div className="echo">
                    <input value={this.state.temperature} onChange={this.handleChange} />
                    <BoilingVerdict celsius={parseFloat(this.state.temperature)} />
                </div>
            );
        }
    }
    
    ReactDOM.render(<Calculator />, document.getElementById('root'));
    

    现在需求升级了,我们知道温度有摄氏度,华氏度不同单位,现在需求是,为用户提供摄氏度与华氏度两个单位的温度输入框,不管用户操作哪一个,另一个温度能自动同步数据展示出对应温度数值。

    由于需要两个温度输入框,这里我们直接将温度输入抽离成一个组件,那么完整的代码为:

    // 这是判断水温有没有烧开的组件
    function BoilingVerdict(props) {
        return props.celsius >= 100 ? <p>水烧开了。</p> : <p>水未烧开。</p>;
    }
    
    const textType = {
        c: '输入摄氏度',
        f: '输入华氏度'
    };
    // 这是抽离的温度组件
    class TemperatureInput extends React.Component {
        constructor(props) {
            super(props);
            this.handleChange = this.handleChange.bind(this);
            this.state = { temperature: 0 };
        }
    
        handleChange(e) {
            this.setState({ temperature: e.target.value });
        }
    
        render() {
            return (
                <div>
                    <p>{textType[this.props.scale]}:</p>
                    <input value={this.state.temperature} onChange={this.handleChange} />
                </div>
            );
        }
    }
    // 这是父组件,目前内部只有两个温度组件,并提供了不同的温度单位类型
    class Calculator extends React.Component {
        render() {
            return (
                <div className='echo'>
                    <TemperatureInput scale="c" />
                    <TemperatureInput scale="f" />
                </div>
            );
        }
    }
    
    ReactDOM.render(<Calculator />, document.getElementById('root'));
    

    可以看到,我们抽离了温度输入框组件后,通过不同type,得到了两个互不影响的温度输入框组件实例。

    但需求是它们两者不管哪一个输入温度,都应该通过对应的单位换算,同步另一方的温度,并判断当前温度的水有没有烧开,所以我们还需要将这两个组件通过某种方式给联系起来。

    OK,我们先准备好摄氏度转换华氏度,与华氏度转为摄氏度的计算方法:

    // 这是华氏度转摄氏度
    function toCelsius(fahrenheit) {
      return (fahrenheit - 32) * 5 / 9;
    }
    // 这是摄氏度转华氏度
    function toFahrenheit(celsius) {
      return (celsius * 9 / 5) + 32;
    }
    

    考虑到输入的值的有效性,官方还提供了一个对于输入值效验的函数:

    /**
     * 尝试转换温度的方法
     * @param {*} temperature 用户输入的温度
     * @param {*} convert 用于计算温度的方法,为toCelsius与toFahrenheit其一
     */
    function tryConvert(temperature, convert) {
        // 将输入的温度转为浮点数,这里的input表示输入,常与output输出一起使用
        const input = parseFloat(temperature);
        // 判断是不是数字,如果不是数字直接返回0
        if (Number.isNaN(input)) {
          return 0;
        }
        // 调用对应的温度计算方法,得到输出的温度
        const output = convert(input);
      	// 保留3位精度的做法
        const rounded = Math.round(output * 1000) / 1000;
        return rounded;
    }
    

    那么到这里,我们已经准备好了温度相互转换的方法,只差状态提升将两个温度输入框组件关联起来了。由于上面的例子中,我们得到了两个温度输入框组件实例,两者都有属于自己的state,相互独立互不影响,前文已经说了状态提升的做法与含义,既然要提升,那自然是要将输入框的state提升到它们共有的且最近的父组件Calculator中。

    通过这种做法,Calculator组件内部将拥有唯一的数据源(state),而摄氏度与华氏度的温度输入组件将分别与Calculator进行数据交互,我们要做的就是当一方温度组件进行修改时,需要在父组件中更新state的同时,再利用当前state的数值,去调用对应的单位换算方法得到对应的温度,再传递给对应温度组件即可。

    所以说到这里,我们需要对上面的例子进行整体的修改,我们直接上完整的代码,再解释做了什么:

    // 这是华氏度转摄氏度
    function toCelsius(fahrenheit) {
        return (fahrenheit - 32) * 5 / 9;
    }
    
    // 这是摄氏度转华氏度
    function toFahrenheit(celsius) {
        return (celsius * 9 / 5) + 32;
    }
    
    /**
     * 尝试转换温度的方法
     * @param {*} temperature 用户输入的温度
     * @param {*} convert 用于计算温度的方法,为toCelsius与toFahrenheit其一
     */
    function tryConvert(temperature, convert) {
        // 将输入的温度转为浮点数,这里的input表示输入,常与output输出一起使用
        const input = parseFloat(temperature);
        // 判断是不是数字,如果输入不是数字直接返回空,比如啥都没输入的情况
        if (Number.isNaN(input)) {
            return '';
        }
        // 调用对应的温度计算方法,得到输出的温度
        const output = convert(input);
        // 保留3位精度的做法
        const rounded = Math.round(output * 1000) / 1000;
        return rounded;
    }
    
    const textType = {
        c: '输入摄氏度',
        f: '输入华氏度'
    };
    function BoilingVerdict(props) {
        // 因为不管操作哪一方,都会同步更新摄氏度,所以我们就用摄氏度来判断水烧开没就行了
        return props.celsius >= 100 ? <p>水烧开了。</p> : <p>水未烧开。</p>;
    }
    
    
    
    class TemperatureInput extends React.Component {
        constructor(props) {
            super(props);
            this.handleChange = this.handleChange.bind(this);
        }
    
        handleChange(e) {
            this.props.onTemperatureChange(e.target.value);
        }
    
        render() {
            return (
                <div>
                    <p>{textType[this.props.scale]}:</p>
                    <input value={this.props.temperature} onChange={this.handleChange} />
                </div>
            );
        }
    }
    
    class Calculator extends React.Component {
        constructor(props) {
            super(props);
            this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
            this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
            this.state = { temperature: '', scale: 'c' };
        }
    
        handleCelsiusChange(temperature) {
            this.setState({ scale: 'c', temperature });
        }
    
        handleFahrenheitChange(temperature) {
            this.setState({ scale: 'f', temperature });
        }
    
        render() {
            // 获取当前操作的温度输入框的单位类型
            const scale = this.state.scale;
            // 获取最新的温度数值
            const temperature = this.state.temperature;
            // 获取当前的摄氏度温度
            const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
            // 获取当前的华氏度温度
            const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
            return (
                <div className='echo'>
                    <TemperatureInput scale="c" temperature={celsius} onTemperatureChange={this.handleCelsiusChange} />
                    <TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} />
                    <BoilingVerdict celsius={parseFloat(celsius)} />
                </div>
            );
        }
    }
    
    ReactDOM.render(<Calculator />, document.getElementById('root'));
    

    代码量貌似有点多,不过没关系,我们来解释下做了什么。

    首先,由于状态提升,我们TemperatureInput所需要的的state提升到了Calculator中,由Calculator统一管理,TemperatureInput不再拥有state,而是接受从Calculator传递过来的props。

    输入框组件只有一个,但事实上我们得到了两个温度输入框实例,无论哪个输入框改变值时,都需要去更新父组件中的state(把当前输入的温度同步过去),所以父组件一定得提供一个更改state的方法给子组件,不然根据props只读性,我们就没办法同步父组件的状态了。

    且为了方便区分当前操作的是哪一个温度输入框,所以我们在父组件中分别定义了两个更新state的方法handleCelsiusChangehandleFahrenheitChange,他们都会调用setState更新温度数值,同时记录当前操作的是哪一种温度。

    最后,在父组件中,我们根据当前操作的温度类型,用于分别同步计算两个温度,比如输入的是摄氏度100,那么摄氏度自身不用参与计算,只用调用toFahrenheit得到对应的华氏度即可,反之亦是如此。

    比较巧妙的是,由于不管是修改摄氏度或者修改华氏度,我们都能得到一个对应的摄氏度,所以只需要将摄氏度传递给BoilingVerdict组件,再利用温度判断,即可得到当前水温是否烧开了。

    叁 ❀ 为什么需要redux?redux与状态提升的区别

    OK,以上就是一个完整的状态提升的例子,它所解决的其实就是兄弟组件共用了一个状态的通信问题。但事实上,一个完整的react应用中需要通信的组件会复杂很多。虽然状态提升也强调了,所谓状态提升,只是将状态提升到离自己最近的父组件上,但实际场景中往往会存在这样的问题:

    比如这个例子中,存在交互的兄弟组件是child3,它们最近的父组件还要往上找三层,那这样就造成了一个问题,每次state同步进行传递时,都需要经过child1与child2。那对于这两兄弟就很头疼了,我们明明不需要这个属性,还要作为媒介帮忙传递props,一层两层还好,层级多了传递起来就难以维护了。

    我们假设层级传递都不高,其实还会存在另外一个问题,如下图:

    在这个例子中,child2与child3有关联,于是状态提升到了parent中,而c4与c5也有关联,它们的状态提升到了child2中,我们可以假象将其关系方法,你会发现state与state之间的关系就像一张复杂的网,我们真的有把握维护好每一个state以及与之关联的state的吗?

    与其将状态提升到就近的父组件,能不能直接将所有状态提升到一个最顶点并由它来统一管理呢?那么这个想法就高度契合redux的设计理念了。

    我们直接通过两张图来对比两者的区别:

    状态提升:

    redux统一管理:

    所以到这里,即便你之前从未了解过vuex与redux,我想redux是用来做什么的在你心中也应该有一个模糊的雏形了,没错,redux就是一个全局的状态管理器,所有的state都被集中存放在Store中,当某个组件状态被修改,便会通知到Store,然后再决定哪些受影响的组件应该重新渲染。

    当然本文并不会立马介绍redux,至于redux如何去使用,应该是下一篇文章应该介绍的事情了。

    那么介绍到这里,你是不是又觉得redux强到不行?其实并不是这样,前面我们也说了,redux是用来解决复杂的组件状态通信,如果你的组件状态更新本身就非常简单,仍然使用redux反而多此一举。

    肆 ❀ 总

    好了,到这里我们介绍了react的状态提升,可以说在未接触redux之前,这就是官方推荐的state管理做法。当然,通过文中的例子,在某些场景下我们也感受到了状态提升的局限性,从而引出了redux的作用,所以下篇文章自然就是介绍redux了,那么到这里,本文结束。

    参考

    react官网 状态提升

    4 张动图解释为什么(什么时候)使用 Redux

    微信读书 深入浅出react和redux 2.4 2.5小节

  • 相关阅读:
    C# 多线程总结 异常处理 线程取消 锁(lock)
    C# sync/async 同步/异步
    C# Parallel 多线程并发
    C# Task TaskFactory 异步线程/异步任务
    C# ThreadPool 线程池
    Nginx基础配置
    Nginx配置通用语法
    Nginx进程间的关系
    Nginx命令行控制
    Flask框架02(路由)
  • 原文地址:https://www.cnblogs.com/echolun/p/14260337.html
Copyright © 2020-2023  润新知