React笔记 - State and Lifecycle

State and Lifecycle

直到现在,我们只学会了一种更新 UI 的方法,调用ReactDOM.render()来改变渲染出的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);

在这一节当中,我们将学习如何真正的封装Clock组件,并且让它变得可以重复使用。

我们可以先把 clock 的样子封装一下,先将它的 DOM 结构构造出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);

然而,这样的做法遗漏了一个最重要的要求:就是我们需要将Clock设置定时器并更新UI的这些功能都在Clock内部实现,理想状态下,我们只需要写一次Clock组件,它就应该可以在其他地方使用,并且自己更新自己。

1
2
3
4
ReactDOM.render(
<Clock />,
document.getElementById('root')
);

为了实现它,我们需要给Clock组件添加”state”。

State is similar to props, but it is private and fully controlled by the component.
state 与 props 相似,但是它是组件私有的,并且完全由组件控制。

之前提到过,class componentfunctional component要多一些特别的功能,state 就是其中之一。

Converting a Function to Class

通过以下5个步骤,你可以将Clock这样的 function 改写成 class:

  1. 用同样的名字,创建一个ES6 class,使其继承自React.Component
  2. 添加一个render()方法
  3. 将 function 的主体全部复制到render()方法中
  4. render()方法中的this.props全部替换成props
  5. 将之前声明的 function 删掉
1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

现在,Clock就从一个 function 变成了一个 class,它也具备了local statelifecycle hooks的额外功能。

Adding Local State to a Class

通过以下的三个步骤,我们可以将date从 props 中移动到 state 里:

  1. render()方法中的this.props.date替换为this.state.date

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Clock extends React.Component {
    render() {
    return (
    <div>
    <h1>Hello, world!</h1>
    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
    </div>
    );
    }
    }
  2. 添加一个class constructor指定初始时的this.state

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Clock extends React.Component {
    constructor(props) {
    super(props);
    this.state = {date: new Date()};
    }
    render() {
    return (
    <div>
    <h1>Hello, world!</h1>
    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
    </div>
    );
    }
    }

这里要注意我们是如何把props传递给父级构造函数的:

1
2
3
4
constructor(props) {
super(props);
this.state = {date: new Date()};
}

class components 必须调用父级构造函数,并且将props作为参数传递给它。

  1. 去掉Clock元素的date属性
    1
    2
    3
    4
    ReactDOM.render(
    <Clock />,
    document.getElementById('root')
    );

我们等会再加上定时器的相关代码,现在,代码应该像是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);

下面,我们将让Clock元素自己设定一个定时器,并且每秒钟更新一次。

Adding Lifecycle Methods to a Class

在有许多的组件构成的应用中,有一点是很重要的,就是能在组件被销毁时释放它们所占用的资源。我们想要在每次Clock被渲染到DOM的时候,启动一个定时器,这在 React 中被称为”mounting”。同样,在由Clock渲染的DOM被移除时,我们也需要清除掉这个定时器,这在 React 中被称为”unmounting”。
在组件的类中,我们可以声明特殊的方法,这些代码会在组件”mount”和”unmount”时被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
// runs after the component output has been rendered to the DOM
}
componentWillUnmount() {
// runs after the component has been removed from DOM
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

这些方法称为”lifecycle hooks”,componentDidMount()方法会在组件被渲染到DOM后执行,在这个方法内设置定时器是很棒的:

1
2
3
4
5
6
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

这里将setInterval()方法返回的 int 值存储在this.timerID中,方便在后面 unmount 的时候,对其使用clearInterval()方法。

While this.props is set up by React itself and this.state has a special meaning, you are free to add additional fields to the class manually if you need to store something that is not used for the visual output.

If you don’t use something in render(), it shouldn’t be in the state.

componentWillUnmount()方法中,销毁掉定时器:

1
2
3
componentWillUnmount() {
clearInterval(this.timerID);
}

最后,我们要实现tick()方法:

1
2
3
4
5
tick() {
this.setState({
date: new Date()
});
}

这个方法会调用this.setState()方法,来安排更新组件的state。
好,我们现在再来回顾一下刚才发生了什么,以及方法被调用的顺序:

  1. <Clock />元素被传递给React.render()方法时,React 会调用Clock组件的构造函数,由于Clock需要展示当前的时间,所以在Clock的构造函数中,this.state初始化了一个包含当前时间的对象,然后我们对它进行更新;
  2. React 接着调用Clock组件的render()方法,这使得 React 知道要在屏幕上展示什么东西。接下来 React 会根据Clockrender()方法来更新 DOM;
  3. Clock的输出被插入到 DOM 中时,React 会调用componentDidMount()方法,在这个方法内部,Clock向浏览器请求设置一个定时器,每秒调用一次组件中的tick()方法;
  4. 每隔一秒钟,浏览器就会调用tick()方法,在这个方法内部,Clock组件通过调用包含当前时间对象的setState()方法来对 UI 进行更新。多亏了setState()方法,React 才能知道组件的状态已经改变了,这时会再次调用render()方法,来确定需要在屏幕上展示出什么。这次,render() 方法中的this.state.date已经不同了,所以渲染出的结果将会包含更新过的时间,React 会对 DOM 进行相对应的更新;
  5. 如果Clock组件从 DOM 中被移除了,React 会调用componentWillUnmount()方法来将定时器停止。

正确使用 State

对于setState()方法,你需要知道以下三件事情:

不要直接修改 State

举个例子,下面的代码不会对组件进行重新渲染:

1
2
// Wrong
this.state.comment = 'Hello';

正确的方法应该是,使用setState()方法对其进行修改:

1
2
// Correct
this.setState({comment: 'Hello'});

唯一能够对this.state进行赋值的地方是构造函数,除此之外,都不可以。

State 的更新有可能是异步的

出于性能方面的考虑,React 有可能将多个setState()请求整合成单独一次的更新。由于this.propsthis.state都可能被异步更新,所以不要依赖它们的值来计算下一个 state。举例来说,下面的代码可能会导致计数器更新失败:

1
2
3
4
// Wrong
this.setState({
counter: this.state.counter + this.props.increment
});

要改正的话,需要用到第二种形式的setState()方法, 这个方法会接收函数作为参数,而不是以对象作为参数。这个匿名方法会接收上一个 state 作为第一个参数,将更新进行时的 props 作为第二个参数:

1
2
3
4
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));

上面使用了arrow function,其实使用常规的函数也是一样的:

1
2
3
4
5
6
// Correct
this.setState(function(prevState, props){
return {
counter: prevState.counter + props.increment
};
});

State Updates are Merged

当你调用setState()方法的时候,React 会将你提供的对象并入(merge into)当前的状态中。举个例子,你的 state 可能会包含多个独立的变量:

1
2
3
4
5
6
7
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}

然后,你可以分开调用setState()方法对它们单独进行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}

The merging is shallow,所以this.setState({comments})会保持this.state.posts的完整,并且整个替换掉this.state.comments


The Data Flows Down

不管是父级组件还是子级组件,都不能知道一个特定的组件到底是有状态的还是无状态的,它们也不应该关心它是定义成一个函数还是一个类。这就是为什么 state 经常被称作是本地的或者是被封装的,除了自己,别的组件是不能对其进行访问的。组件可以选择将自己的 state 作为 props 传递给它的子级组件:

1
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

对于用户定义的组件,也是可以的:

1
<FormattedDate date={this.state.date} />

FormattedDate组件将会接收到一个包含在其 props 中的date对象,它不知道这个对象是来自Clock的 state,还是Clock的 props,或者是硬编码进去的:

1
2
3
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString}.</h2>;
}

这通常被称作“自顶向下的”或“单向的”数据流,任何的 state 都永远是为某些特定的 component 所有的,并且任何的从那个 state 处得到的数据或者 UI 只能影响它们“下面的”组件。

如果你把一个组件树想象成由 props 构成的瀑布的花,每个组件的 state 就像一个额外的水源,它在某个任意的点上汇入,但是也将“顺流而下”。

为了展示所有的组件都是真正独立的,我们可以创建一个App组件来渲染3个<Clock />

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);

这样,每个Clock都会单独地设置自己的定时器并且更新。

在 React apps 中,考虑一个组件是有状态的还是无状态的,涉及到一个实现的细节,即这个组件是否会随着时间而发生变化。你既可以在有状态的组件中使用无状态的组件,也可以在无状态的组件中,使用有状态的组件,这都是可以的。