Skip to content

Latest commit

 

History

History
320 lines (244 loc) · 13 KB

【译】快速起步-状态提升.md

File metadata and controls

320 lines (244 loc) · 13 KB
title date version
快速起步-状态提升
2017-04-25 10:27:42 -0700
15.5.0

状态提升

通常,多个组件会响应同样的变化数据。我们建议将这种共享的状态提升到最近的共同的祖先。让我们看看它是如何工作的。

在这一节中,我们会创建一个温度计算器用来计算水是否在给定的温度下沸腾。

我们先创建一个 BoilingVerdict(沸腾判断)组件。它有一个叫 celsius (摄氏温度)的属性,并会打印水是否沸腾:

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

下一步,我们创建一个叫 Calculator(计算器)的组件。它会渲染一个可让你输入摄氏温度的输入框,并将该输入框的值绑定到 this.state.temperature 上。

另外,它也会通过输入框的值渲染 BoilingVerdict 组件。

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }

  handleChange(e) {
    this.setState({temperature: e.target.value});
  }

  render() {
    const temperature = this.state.temperature;
    return (
      <fieldset>
        <legend>Enter temperature in Celsius:</legend>
        <input
          value={temperature}
          onChange={this.handleChange} />
        <BoilingVerdict
          celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}

Try it on CodePen.

添加第二个输入

我们的新需求是,我们不仅要提供摄氏温度输入,还需要提供华氏温度输入,并要自动进行转换。

我们从 Calculator 提取出一个叫 TemperatureInput 的组件,并运行传入 "c" 或者 "f" 给它的属性 scale

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }

  handleChange(e) {
    this.setState({temperature: e.target.value});
  }

  render() {
    const temperature = this.state.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

现在我们来更新 Calculator,让它渲染两个独立的温度输入组件:

class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}

Try it on CodePen.

现在,我们有两个输入框了,但是当我们在其中一个输入值时,另外一个并不会更新。这违背了我们的需求,我们想让它们保持同步。

此时,我们也不能从 Calculator 中展示 BoilingVerdict。 因为 Calculator 并不知道当前的温度,温度是隐藏在 TemperatureInput 组件内部的。

编写转换功能

首先,我们先编写两个函数来对华氏温度和摄氏温度进行转换:

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}

这两个函数将会转换不同的温度。我们还需要编写另一个函数,它将字符串 temperature(温度)和一个转换器作为参数,并返回一个字符串。我们将使用它来根据一个输入计算另外一个输入。

temperature(温度)不合法时,它会返回一个空字符串,它也会四舍五入到小数点后第三位:

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

比如,tryConvert('abc', toCelsius) 返回空字符串,tryConvert('10.22', toFahrenheit) 返回 '50.396'

状态提升

目前,所有的 TemperatureInput 组件都将值作为本地状态保存在组件内部:

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }

  handleChange(e) {
    this.setState({temperature: e.target.value});
  }

  render() {
    const temperature = this.state.temperature;

然而,我们希望它们能够相互同步这个值。当我们更新摄氏温度时,华氏温度也会自动显示,反之亦然。

React 中,共享状态是通过将状态移动的最近的共同祖先来实现的。这被称之为 “状态提升”。我们将从 TemperatureInput 组件中移除本地状态,并在 Calculator 组件中保存状态。

如果 Calculator 拥有共享状态,它就成为了多个温度输入组件的的 “source of truth”(译者理解:唯一来源)。它可以让输入组件具有彼此一致的值。自从将 TemperatureInput 组件的属性提取到共同的父组件 Calculator 后,它们的输入值会总是同步的。

我们看看看它是如何一步步开始工作的。

首先,我们会在 TemperatureInput 组件中,使用 this.props.temperature 来替换 this.state.temperature。现在,先假设 this.props.temperature 总是存在的。将来,我们会在 Calculator 组件中传递给它:

  render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;

我们知道 属性是只读的。当 temperature 是本地状态时,TemperatureInput 组件通过 this.setState() 来改变它。然而,temperature 成为了父组件的属性,TemperatureInput 组件就没有该属性的控制权了。

React 中,通常让组件 controlled 来解决该问题。就像DOM中的 <input> 接收一个 valueonChange 属性,自定义的 TemperatureInput 会从 Calculator(父组件)中接收 temperatureonTemperatureChange

现在,当 TemperatureInput 想更新它的温度,可以调用 this.props.onTemperatureChange

  handleChange(e) {
    // Before: this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);

注意自定义组件中的 temperatureonTemperatureChange 没有特殊的含义。我们也可以用其他名称代替,比如 valueonChange 这种常用的惯例。

组件中的 onTemperatureChange 属性将会通知父组件 Calculator 温度变化。父组件将通过修改自己的本地状态来处理更新,并为两个输入组件提供新的值。我们将很快看到新的 Calculator 实现。

在更新 Calculator 之前,我们先更新 TemperatureInput。先移除它的本地状态,使用 this.props.temperature 来替代this.state.temperature。同时,我们调用 Calculator 提供的 this.props.onTemperatureChange() 来替代自身的 this.setState()

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

现在,我们切换到 Calculator 组件。

我们将当前输入组件的 temperaturescale 存储到本地状态中。这是从输入组件中提升的状态,并为输入组件提供真实的值。为了呈现两个输入,我们需要知道所有数据的最小表示。

例如,我们在摄氏温度中输入了 37 ,Calculator 组件的状态将会是:

{
  temperature: '37',
  scale: 'c'
}

如果我们之后在华氏温度中输入了 212,Calculator 组件的状态将会是:

{
  temperature: '212',
  scale: 'f'
}

我们可以存储两个输入的值,但实际上是不必要的。存储最近更改的输入值,以及它所代表的比例就足够了。我们可以基于当前的温度(temperature)和温度类别(scale)来推断另一个输入的值。

这将是输入保持同步,因为它们的值是从相同的状态计算出来的:

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>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

Try it on CodePen.

现在,无论你是在哪个温度输入组件中输入,Calculator 组件中的 this.state.temperaturethis.state.scale 都会更新。其中一个输入值,另一个的输入值总是基于它重新计算,所以随便输入哪个,值都会生效。

让我们回顾一下编辑输入框时会发生什么:

  • React会调用DOM元素 <input> 上的 onChange 函数。在我们的示例中, 是 TemperatureInput 组件的 handleChange 方法。
  • TemperatureInput 组件的 handleChange 方法会调用 this.props.onTemperatureChange() 并传入新的期望值。这是由父组件 Calculator 传递来的属性。
  • 在渲染呈现之前,如果在 TemperatureInput 组件中输入摄氏温度,onTemperatureChange 将会调用父组件的 handleCelsiusChange 方法,如果是输入的华氏温度,onTemperatureChange 将会调用父组件的 handleFahrenheitChange 方法。 因此,会根据具体的输入,调用这两个方法中的一个。
  • 在这些方法内部,Calculator 组件通过 this.setState() 通知 React 使用最新的输入值来重新渲染它自己。
  • React 调用 Calculator 组件的 render 方法来确定需要展示的UI。所有输入框的值,都将通过当前温度和温度类型来重新计算。温度就在这个阶段进行转换的。
  • Calculator 传递给 TemperatureInput 的属性发生变化时,React调用 TemperatureInput 组件的 render() 方法来确定如何渲染。
  • 最终,React DOM 使用期望的输入值,来更新DOM。在我们更新一个的时候,另一个输入框也就同步更新。

每个更新都会执行相同的步骤,以便输入保持同步。

经验教训

对于在 React 中更改的任何数据,应该有一个唯一的来源。通常,状态首先会被添加到使用它的组件中。然后,当其他组件也需要它的时候,可以将它提升到最近的公共祖先上,而不是尝试同步这些状态。我们应该使用 单向数据流

提升状态会比双向绑定编写更多的样板代码,但它有一个好处,不容易制造bug。提升状态到单一组件后,仅有该组件可以修改它,这导致错误的可能性大大降低。此外,您可以实现任何自定义的逻辑来拒绝或者转换用户的输入。

如果一些东西可以通过 props 或者 state 得到,那么他可能并不需要放在共享状态中。例如,我们并没有存储 celsiusValuefahrenheitValue,而是存储最后编辑的 temperaturescale。其他的值总是可以在 render() 中被计算出来。这使得我们可以清理或者四舍五入到其他字段,而不会在用户输入中丢失精度。

当你在UI中看到错误时,您可以使用 React Developer Tools 来检查属性,并向上查找组件树直至到负责更新的组件上。这可以让你跟踪这些错误来源:

Monitoring State in React DevTools