React教程 - 5. 状态和生命周期

本文译自React官方文档
全文翻译及相关代码,请参看我的Github

回顾之前学习的时钟的例子,元素用于描述我们想要在屏幕上看到的内容。
我们调用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);

在CodePen中尝试
在本部分,我们将学习如何使Clock组件变得可重用,它会建立自己的定时器并每秒自动进行更新。
我们可以先从封装Clock的样式着手:

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);

在CodePen中尝试
但是,这里漏了一个很重要的地方:”Clock建立一个定时器并每秒更新UI”这一行为事实上应该是由Clock自己实现的(而不是将new Date作为属性传入)。
理想情况下它应该是这样的:

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

为了完成这个目的,我们应该为Clock组件增加状态(state)
状态与属性(props)类似,但它是私有的,且完全由组件控制。
我们之前提到过,被定义为class的组件拥有额外的特性,本地状态就是其中之一-一个只有类组件拥有的特性。

1.将函数式组件转换转为类组件

我们可以通过5步将Clock组件从函数式转换为类组件:

  1. 创建一个继承自React.ComponentES6 class
  2. 为其增加一个叫做render()的方法
  3. 将函数式组件的内容移至render()方法中
  4. 在移动的过程中,将原来的props转为this.props
  5. 删除剩余的空函数声明
    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>
    );

    }
    }

在CodePen中尝试
Clock组件现在从函数式组件变成了类组件,这使我们能够使用诸如state和Lifecycle Hooks等额外属性。

2.为类组件增加本地状态

我们通过以下3个步骤将date从props变成state:

  1. render()中使用this.state.date替换this.props.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. 增加一个构造函数初始化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()};
    }

    类组件应该总是在构造函数中执行super(props)这一过程

  3. 删除<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')
);

在CodePen中尝试
接下来,我们要为Clock组件添加定时器使其每秒自动更新。

3.为类组件增加生命周期方法

在有多个组件的应用中,组件销毁时及时释放其所占用的资源是非常有必要的。
我们想要在Clock被渲染到DOM后立即建立一个定时器,这在React中是mount阶段。
我们还想在当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() {

}

componentWillUnmount() {

}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);

}
}

这些方法被称为”Lifecycle Hooks”
当一个组件被渲染至DOM时执行componentDidMount,这是一个设置定时器的好时机:

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

注意,我们在这里将定时器的ID保存在了this中。
this.props由React自己建立,this.state有着特殊的意义-当有某些不是用于视觉输出的数据需要存储时,我们可以将其保存在class中。
如果这个数据不是用于render()中的,就不应该添加在state里。
componentWillUnmount阶段,我们将销毁定时器:

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

最后,我们需要完成每秒都执行的tick()方法。
该方法通过this.setState()更新组件的本地状态(local state):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

componentWillUnmount() {
clearInterval(this.timerID);
}

tick() {
this.setState({
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')
);

在CodePen中尝试
现在这个时钟能够开始动起来了。
让我们快速回顾一下发生了什么,以及各方法的调用顺序:

  1. <Clock />传递给ReactDOM.render()时,React调用了<Clock />的构造函数。
    由于Clock要显示当前时间,其将this.state初始化为一个包含当前时间的对象。我们将在稍后更新该state。
  2. React之后调用Clock组件的render()方法,根据该方法在屏幕上绘制UI。
  3. Clock输出被插入DOM时,React调用componentDidMount Life Hook。在该hook中Clock组件通知浏览器建立一个定时器并每秒调用一次tick()
  4. 浏览器每秒调用一次tick()方法。在该方法中,Clock组件通过调用setState()安排一次UI更新。
    setState()的调用让React收到状态改变的通知,并再次执行render()方法了解如何在屏幕上绘制UI。这次,render()方法中的this.state.date发生了变化,因此渲染输出包含更新了的时间。React根据它来更新DOM。
  5. 如果<Clock />组件从DOM中移除了,React会调用componentWillUnmount Life Hook,定时器将会被停止。

4.正确使用状态(State)

关于setState(),我们需要知道三点
不要直接改变state
例如,这种操作无法重新渲染一个组件:

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

应该使用setState()

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

我们唯一能直接给this.state赋值的地方是在组件的构造函数中。

状态更新可能是异步的
从性能的角度考虑,React可能会在某次单独的更新中批量执行多个setState()
由于this.propsthis.state可能会异步更新,我们不应该依靠他们的值来计算下一步的状态。
例如,以下代码可能无法准确的更新counter:

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

为避免这个问题,可以使用setState()的另一种形式-接受函数而不是对象。传递给setState的函数将之前的状态作为第一个参数,需增加的属性作为第二个参数:

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

我们在这里使用了箭头函数,这么写也是一样的:

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

状态更新是合并更新
调用setState()时,React将我们提供的对象合并至当前状态。
例如,我们的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
});
});
}

调用this.setState({comments})不会修改this.state中posts的值,而是仅仅修改this.state.comments的内容。

5.数据流向下

父组件或子组件都无法知道某组件是否是有状态的,它们也不应该组件是函数式组件还是类组件。
这也是为什么state通常是被本地调用或封装-它只能被拥有并建立它的组件访问,其他组件均不能访问它。
组件可以将state作为props传递给子组件:

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

也可以这么做:

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

FormattedDate组件将date作为其属性,但并不知道这个date的来源(是来自Clock的状态、Clock的属性或手动设置的):

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

在CodePen中尝试
这通常被称为“自顶向下”或“单向”数据量。任何state都是由某个特定的组件拥有的,并且继承自该状态的任何数据和UI只能影响在他们组件树之下的组件。
将组件树想象为一个属性的瀑布,每个组件的状态(state)就好比是在任意节点中加入的,同样是自上而下的额外水源。
为了表示所有的组件都是真正隔离的,我们可以创建一个App组件渲染三个<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')
);

在CodePen中尝试
每个Clock建立自己的定时器并独立更新。
在React应用中,组件是否有状态被认为是组件的实现细节,其可能会随着时间而改变。我们可以在有状态的组件中使用无状态的组件,反之亦然。

本文首发于http://www.miaoyunze.com/,转载请注明出处