w3ctech

React 初学者教程 8:处理状态

到目前为止,我们已经创建的组件都是无状态的(stateless)。它们有从它们的父组件传递进来的属性(亦称 props),但是一旦组件活跃起来,就没有什么能改变它们。一旦属性被设置了,就被当作是不可修变的(immutable)。对于很多交互场景来说,你肯定不想这样子。在一些用户交互发生,或者一些数据从服务器或者其它地方返回过来时,你想能改变组件的外观。

我们所要的是另一种超越属性的在组件上存储数据的方式,即存储可以修改的数据。我们所需要的是称为状态(state)的东西。在本教程中我们打算学习有关状态的一切,以及如何使用状态来创建有状态的组件。

使用状态

如果你知道如何处理属性,就基本上也知道了如何处理状态。虽有二者之间有些不同,但是很不明显,现在就先不用马上啰嗦,我们直接在一个小例子中使用状态。

我们要创建一个简单的雷击计数示例:

这个示例所做的没啥特殊的。地球表面的雷击大约是一秒100次。我们有一个计数器增加你看到的数字。

起点

本例的主要焦点是看看如何处理状态。前面我们已经做过很多个示例,这里我们就不打算从头开始创建了。只需要修改以前的 HTML 文档,修改为以下代码即可:

<!DOCTYPE html>
<html>

<head>
  <title>React! React! React!</title>
  <script src="https://fb.me/react-0.14.7.js"></script>
  <script src="https://fb.me/react-dom-0.14.7.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
</head>

<body>
  <div id="container"></div>
  <script type="text/babel">
    var LightningCounter = React.createClass({
      render: function() {
        return (
          <h1>Hello!</h1>
        );
      }
    });

    var LightningCounterDisplay = React.createClass({
        render: function() {

          var divStyle = {
            width: 250,
            textAlign: "center",
            backgroundColor: "black",
            padding: 40,
            fontFamily: "sans-serif",
            color: "#999",
            borderRadius: 10
          };

          return(
            <div style={divStyle}>
              <LightningCounter/>
            </div>
          );
        }
    });

    ReactDOM.render(
      <LightningCounterDisplay/>,
      document.querySelector("#container")
    );
  </script>
</body>

</html>

完成后预览一下,你会看到如下的结果:

这时候,花几分钟看看代码。首先,我们有一个组件 LightningCounterDisplay:

var LightningCounterDisplay = React.createClass({
    render: function() {

      var divStyle = {
        width: 250,
        textAlign: "center",
        backgroundColor: "black",
        padding: 40,
        fontFamily: "sans-serif",
        color: "#999",
        borderRadius: 10
      };

      return(
        <div style={divStyle}>
          <LightningCounter/>
        </div>
      );
    }
});

这个组件的绝大部分代码是负责圆角背景的样式信息的 divStyle 对象。 return 函数返回一个包围 LightningCounter 组件的 div 元素。

LightningCounter 组件是所有行为要发生的地方:

var LightningCounter = React.createClass({
  render: function() {
    return (
      <h1>Hello!</h1>
    );
  }
});

这个组件目前只是返回单词 'Hello!',稍后我们会修改。

最后,我们来看看 ReatDOM.render 方法:

ReactDOM.render(
  <LightningCounterDisplay/>,
  document.querySelector("#container")
);

它只是将 LightningCounterDisplay 组件推到 DOM 中的 container 元素。差不多就是这样子。我们看到的最终结果是,来自ReactDOM.render 方法、 以及LightningCounterDisplay 和LightningCounter 组件的标记组合。

让计数器跑起来

现在我们已经完成了基础代码,我们可以开始规划下一步了。 counter 工作的方式很简单。我们打算用 setInterval 函数每 1000 微秒(即 1秒)调用一段代码。这段代码每被调用一次,就让一个值增 100。看起来很简单,对吧?

要让这些都可以工作起来,我们打算依赖于 React 组件暴露的三个 API:

  1. componentDidMount 该方法在组件渲染后被调用(or mounted as React calls it).。
  2. getInitialState 该方法在组件挂载之前运行,并允许你修改组件的 state 对象。
  3. setState 该方法允许你修改 state 对象的值。

我们不久就会看到这些 API 的使用,但是这里我先预习一下,这样子你就可以很容易认出它们!

设置初始状态值

我们需要一个变量充当计数器,我们称它为 strikes。创建这个变量有很多方法,最简单的方式是这样:

var strikes = 0 // :P

当然,我们肯定不想这样做。对于我们的示例,strikes 变量是组件状态的一部分,它的值就是在屏幕上显示的值。我们要做的是在 getInitialState 方法内初始化 strikes 变量:

var LightningCounter = React.createClass({
  getInitialState: function() {
    return {
      strikes: 0
    };
  },
  render: function() {
    return (
      <h1>{this.state.strikes}</h1>
    );
  }
});

getInitialState 方法会自动在组件渲染之前运行,我们要做的是告诉 React 返回一个包含 strikes 属性(初始化为0)的对象。你也许会想我们返回这个对象给谁?所有这一切都是在幕后神秘地发生的。返回的对象是被设置为组件的 state 对象的初始值。

如果在这段代码运行后检测 state 对象的值,它看起来会是这样子的:

var state = {
  strikes: 0;
}

在完成本节之前,我们来让 strikes 属性可以看到。在 render 方法中,修改代码如下:

var LightningCounter = React.createClass({
  getInitialState: function() {
    return {
      strikes: 0
    };
  },
  render: function() {
    return (
      <h1>{this.state.strikes}</h1>
    );
  }
});

这里我们把默认的 'Hello!' 文本替换为一个显示 this.state.strikes 属性存储的值的表达式。如果在浏览器中预览,你会看到显示的值是 0。这是一个开始!

启动定时器并设置状态

下一步是让定时器运行起来,增加 strikes 属性。像我们前面提到的一样,我们将用 setInterval 函数每秒钟增加 strikes 属性 100。我们打算在组件被渲染后用内置的 componentDidMount 方法立即做所有这事。

启动定时器的代码如下:

var LightningCounter = React.createClass({
  getInitialState: function() {
    return {
      strikes: 0
    };
  },
  componentDidMount: function() {
    setInterval(this.timerTick, 1000);
  },
  render: function() {
    return (
      <h1>{this.state.strikes}</h1>
    );
  }
});

一旦组件被渲染了,componentDidMount 方法就被调用,在该方法内,setInterval 方法每 1000 微秒调用 timerTick 函数一次。

我们还没有定义 timerTick 函数,现在添上:

var LightningCounter = React.createClass({
  getInitialState: function() {
    return {
      strikes: 0
    };
  },
  timerTick: function() {
    this.setState({
      strikes: this.state.strikes + 100
    });
  },
  componentDidMount: function() {
    setInterval(this.timerTick, 1000);
  },
  render: function() {
    return (
      <h1>{this.state.strikes}</h1>
    );
  }
});

timerTick 函数做的事情很简单,只是调用 setState。setState 方法有很多种用法,但是对于我们这里所做的,它只是带一个对象为其参数。这个对象包含有所有我们像合并到 state 对象的属性。在本例中,我们是指定 strikes 属性,并且设置它的值为当前值加上100。

渲染状态改变

如果在浏览器中预览,你会看到 strikes 值每秒钟增100:

我们先把代码放一边,这代码很简单。有意思的事情是我们所做的事情是如何最终会导致我们在屏幕上看到的更新。这种更新是因为 React 的机制:只要你调用 setState 并更新了 state 对象中一些东西,那么组件的 render 方法就会自动被调用。这就xxxx级联调用了输出会受到影响的任何组件的 render 方法(This kicks of a cascade of render calls for any component whose output is also affected)。所有这一切的最终结果是,我们在屏幕上看到的应用程序的 UI 状态的最新表示。在 UI 开发中,让数据和 UI 同步是最困难的事情之一,所以 React 负责这事对我们来说是好事情。它让我们学习使用 React 的一切痛苦完全值得 :P

可选:完整的代码

完整代码如下:

var LightningCounter = React.createClass({
  getInitialState: function() {
    return {
      strikes: 0
    };
  },
  timerTick: function() {
    this.setState({
      strikes: this.state.strikes + 100
    });
  },
  componentDidMount: function() {
    setInterval(this.timerTick, 1000);
  },
  render: function() {
    var counterStyle = {
      color: "#66FFFF",
      fontSize: 50
    };

    var count = this.state.strikes.toLocaleString();

    return (
      <h1 style={counterStyle}>{count}</h1>
    );
  }
});

var LightningCounterDisplay = React.createClass({
    render: function() {
      var commonStyle = {
        margin: 0,
        padding: 0
      }
      var divStyle = {
        width: 250,
        textAlign: "center",
        backgroundColor: "#020202",
        padding: 40,
        fontFamily: "sans-serif",
        color: "#999999",
        borderRadius: 10
      };

      var textStyles = {
        emphasis: {
          fontSize: 38,
          ...commonStyle
        },
        smallEmphasis: {
          ...commonStyle
        },
        small: {
          fontSize: 17,
          opacity: 0.5,
          ...commonStyle
        }
      }

      return(
        <div style={divStyle}>
          <LightningCounter/>
          <h2 style={textStyles.smallEmphasis}>LIGHTNING STRIKES</h2>
          <h2 style={textStyles.emphasis}>WORLDWIDE</h2>
          <p style={textStyles.small}>(since you loaded this example)</p>
        </div>
      );
    }
});

ReactDOM.render(
  <LightningCounterDisplay/>,
  document.querySelector("#container")
);

如果你的代码跟上面一样,并再次运行该示例,就会看到如图 8.1 的高亮度计算器示例。

总结

我们只是肤浅地研究了如何创建有状态的组件。虽然用定时器来更新 state 对象很酷,但是实际行为是在我们开始将用户交互与状态组合时才发生的。迄今为止,我们回避了大量的鼠标、触摸、键盘以及组件将会接触到的其它相关事情。在下一个教程中,我们将解决这个问题。沿着这条路,你会看到我们把我们已经看到的有关状态的技术带到一个全新的层次!如果这还不能让你激动,那么我不知道什么可以 :p

w3ctech微信

扫码关注w3ctech微信公众号

共收到1条回复

  • 最后的完整代码,为什么要加这句代码:var count = this.state.strikes.toLocaleString(); 尝试去掉这行代码也可以,在这里这句代码有什么特殊用意吗?

    回复此楼