React文档 state and lifecycle

状态和生命周期

这篇介绍 React 组件中状态和声明周期的概念。详情可以查看API参考 。

思考前一部分中时钟的例子。渲染元素中,我们仅学习了一种更新 UI 的方式。调用 ReactDOM.render() 改变渲染后的输出。

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

在线尝试

这部分,我们学习如何编写真正可复用的封装 **Clock ** 组件。 它会设置自己的计时器每秒更新自己。

我们从封装 时钟的外层开始:

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

在线尝试

但这个忽视了一个最重要的需求: Clock 创建一个计时器且每秒更新自身 UI 应该是一个 Clock 的细节实现。

理想情况下我们写一次让 Clock 更新自身:

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

实现这一需求我们需要给 Clock 组件添加 "state".

State 很类似 props, 不同的是它完全私有由组件控制。

函数转化为类

转化一个类似 Clock 的函数组件为类组件需要五步:

  1. 创建一个 ES6标准的类, 名称不变,继承 React.Component.

  2. 重写 **render()**方法。

  3. 函数的祖逖移到 render() 方法中。

  4. render() 方法体重使用 this.props替换 props

  5. 删除空的函数声明。

1class Clock extends React.Component { 2 render() { 3 return ( 4 <div> 5 <h1>Hello, world!</h1> 6 <h2>It is {this.props.date.toLocaleTimeString()}.</h2> 7 </div> 8 ); 9 } 10} 11

在线尝试

函数组件定义的 Clock 现在由类组件定义。

render() 方法会在每次更新时调用,但是只要我们渲染 <Clock /> 到同样的 DOM 节点,就会使用 Clock 类的单一实例。这让我们可以使用如 local state 和 lifecycle 钩子等额外的特性。

为类添加本地状态

三步把 date 从 props 移动到 state:

  1. render() 方法中使用 this.state.date 替换 this.props.date

1class Clock extends React.Component { 2 render() { 3 return ( 4 <div> 5 <h1>Hello, world!</h1> 6 <h2>It is {this.state.date.toLocaleTimeString()}.</h2> 7 </div> 8 ); 9 } 10} 11
  1. 添加一个类构造器来分管 this.state 的初始化:

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

注意我们传递 props 到基础构造器:

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

类组件总是通过 props 调用基础构造器。

  1. 移除**<Clock />** 元素中的 date props :

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

待会我们在将计时器代码回写到 组件本身。

结果如下:

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

在线尝试

下面,我们编写 Clock 设置他自己的计时器每秒更新自身。

为类添加声明周期方法

多个组件的应用中,当组件销毁时释放组件占用的资源非常重要。

我们想 设置一个计时器 无论何时 Clock 第一次被渲染到 DOM. React 中称之为 ”mounting".

我们也想 清楚一个计时器 无论何时 Clock 被DOM 移除。 React 中称之为 “unmounting".

当组件 mounts 和 unmounts 时我们可以在组件类声明特殊的方法来运行。

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

这些方法称为 "lifecycle hooks(生命周期钩子)".

componentDidMount() 钩子在组件输出渲染至DOM 后运行。这个位置很适合建立一个计时器:

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

注意我们如何正确的将 计时器 ID 保存到 this.

当 React 设置 this.props this.state 有了特别的含义,你可以随意为类手动添加额外字段,如果你需要保存一些不参与数据流(比如 timerID)。

我们在 componentWillUnmount() 生命周期钩子函数中去掉 计时器。

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

最后我们,实现一个称为 tick() 的方法实现 Clock 组件每秒运行。

这会用到 this.setState() 来调度更新组件的本地状态:

1class Clock extends React.Component { 2 constructor(props) { 3 super(props); 4 this.state = {date: new Date()}; 5 } 6 7 componentDidMount() { 8 this.timerID = setInterval(() => this.tick(), 1000); 9 } 10 11 componentWillUnmount() { 12 clearInterval(this.timerID); 13 } 14 15 tick() { 16 this.setState({ 17 date: new Date() 18 }); 19 } 20 21 render() { 22 return ( 23 <div> 24 <h1>Hello, world!</h1> 25 <h2>It is {this.state.date.toLocaleTimeString()}.</h2> 26 </div> 27 ); 28 } 29} 30 31ReactDOM.render( 32 <Clock />, 33 document.getElementById('root') 34); 35

在线尝试

现在时钟按秒运行。

快速的回顾下发生了什么还有方法调用顺序:

  1. <Clock /> 传给 ReactDOM.render(), React 调用Clock 组件的构造器。从 Clock 需要显示当前时间,它通过包含当前时间的对象初始化 this.state。稍后更新 state.
  2. React 调用 Clock 组件的 render() 方法,知道应该在屏幕上显示什么。React 之后更新 DOM 来匹配 Clock 的渲染后输出。
  3. Clock 的输出被插入 DOM, React 调用 **componentDidMount()**生命周期钩子。在方法内部,Clock 组件让浏览器设置一个计时器按秒来调用组件的 **tick()**方法。
  4. 浏览器按秒调用 tick() 方法。其中,Clock组件通过包含当前时间的对象调用setState() 来调度 UI 更新。React通过 setState() 犯法调用知晓组件状态发生改变,随后调用 render() 方法再次知晓屏幕上应该显示什么。这时,render() 方法中 this.state.date 会发生改变,因此渲染结果将会包含更新后的时间。React 相应的更新 DOM.
  5. 如果 Clock 组件一旦移除DOM, React 调用 componentWillUnmount() 生命周期钩子,然后计时器停止。

正确的更新 State

关于setState() 的三个须知:

不要直接修改修改 State

例如,这样不会 重新渲染一个组件:

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

应该使用 setState():

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

构造器是唯一可为 this.state 赋值的地方。

State 更新可能同步

React 为了性能可能批量多次 调用 setState() 一个单独的更新。

因为 this.propsthis.state 可能异步更新,不应该依赖它们的值来计算下一个状态。

例如,如下代码可能更新计数器失败:

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

修复这个问题,使用 setState() 的第二种形式,接受函数而不是一个对象。这个函数接受之前的 state 作为第一个参数, 当时间更新时 props 作为第二个参数。

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

上面的例子中我们使用了 箭头函数,但常规函数也是可以的。

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

状态更新合并

当调用 setState(), React会合并你提供给当前状态的对象。

例如, 你的状态可能包含多个独立的变量:

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

那么你可以通过分别调用 **setState()**独立更新他们:

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

合并是浅的, 所以 **this.setState({comments}) ** 保留了 this.state.posts 的完整,却完全替换了 this.state.comments.

数据流向下

无论子组件还是父组件都无法知道一个特定的主键是有状态还是无状态,而且他们也不应当关心它是用函数方式还是类方式定义。

这是为什么 state 经常被成为本地或者被封装的。它对任何组件不可达,无论是组件拥有它或者是其组成部分。

一个组件可能选择传递他的 state 向下作为 props 给它的子组件:

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

这对用户自定义组件也同样有效:

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

FormattedDate 组件接受它 props 中的 date,并不知道他来自 Clock的 state,还是来自 Clock 的 props, 或者是手动输入:

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

在线尝试

这个通常称为 "top-down(自上而下)" 或者 "unidirectional(单向)" 数据流。任何 state 总数被特定的组件所拥有,任何通过 state 传递的数据或 UI 都只能影响树形结构的下方组件。

你可以家乡组件树是一个 props 瀑布,每个组件的状态就像一个额外的水源在随机点加入它同时向下流。
为展示所有组件真正独立,我们创建一个 App 组件 渲染三个 <Clock />:

function App() {
return (

1<div> 2 <Clock /> 3 <Clock /> 4 <Clock /> 5</div> 6); 7

}

ReactDOM.render() {
<App />,
document.getElementById('root')

1[在线尝试]() 2 3每个 **Clock** 设置他自己的计时器独立更新他们。 4 5React 应用中,无论一个组件是有状态还是无状态都被当做一个组件可随时间改变的实现细节。 6你可在有状态组件中使用无状态组件,反之亦然。 7 8

代码交流 2021