从零开始的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小节

原文地址:https://www.cnblogs.com/echolun/p/14260337.html