008-状态和生命周期
一、概述
时钟示例
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
组件真正可重用和封装。它会设置自己的计时器并每秒更新一次。
从封装时钟的外观开始:
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
更新本身:
ReactDOM.render( <Clock />, document.getElementById('root') );
为了实现这个,我们需要给组件添加“state” Clock
。
状态类似于props,但是它是私人的并且完全由组件控制。
定义为类的组件具有一些附加功能。本地状态就是这样:一个仅适用于类的功能。
1.1、将函数转换为类
您可以通过Clock
五个步骤将功能组件转换为类:
-
创建一个扩展名为ES6的类,名称相同,继承自
React.Component
。 -
为它添加一个空的方法
render()
。 -
将函数的主体移到
render()
方法中。 -
更换
props
用this.props
的render()
身体。 -
删除剩余的空函数声明。
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div> ); } }
Clock
现在被定义为一个类而不是一个函数。
这让我们可以使用附加功能,例如本地状态和生命周期挂钩。
1.2、将本地状态添加到类
将date从props转为state主要有三步:
1、在该方法中替换this.props.date
为:this.state.date
render()
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
2、添加一个指定初始值的类构造函数this.state
:【注意传递props
给基础构造函数】
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
。
3、date
从<Clock />
元素中移除prop :
ReactDOM.render( <Clock />, document.getElementById('root') );
结果为:
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') );
1.3、将生命周期方法添加到类
在具有多个组件的应用程序中,释放组件在销毁时所占用的资源非常重要。
我们想要在第一次呈现给DOM 时设置一个计时器Clock
。这在React中被称为“挂载”。
我们也想清除该定时器,只要Clock
删除由该DOM生成的DOM 。这在React中被称为“卸载”。
我们可以在组件类上声明特殊的方法来在组件装载和卸载时运行一些代码:
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> ); } }
这些方法被称为“生命周期挂钩”。
componentDidMount()
在将组件输出呈现给DOM后,该钩子运行。这是设置计时器的好地方:
componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 ); }
注意我们如何保存定时器ID this
。
虽然this.props
由React自己设置并this.state
具有特殊含义,但如果您需要存储不参与数据流的内容(如计时器ID),则可以手动将其他字段添加到类中。
我们将拆除componentWillUnmount()
生命周期钩子中的计时器:
componentWillUnmount() { clearInterval(this.timerID); }
最后,我们将实现一个调用的方法tick()
,该Clock
组件将运行每一秒。
它将this.setState()
用于安排组件本地状态的更新:
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') );
调用方法的顺序:
-
当
<Clock />
传递给ReactDOM.render()
React 时,React调用Clock
组件的构造函数。由于Clock
需要显示当前时间,因此它会this.state
使用包含当前时间的对象进行初始化。我们稍后将更新这个状态。 -
React然后调用
Clock
组件的render()
方法。这就是React如何学习屏幕上应显示的内容。React然后更新DOM以匹配Clock
渲染输出。 -
当
Clock
输出插入到DOM中时,React调用componentDidMount()
生命周期钩子。在它里面,Clock
组件要求浏览器设置一个计时器,tick()
每秒调用一次该组件的方法。 -
浏览器每秒调用一次该
tick()
方法。在它内部,Clock
组件通过调用setState()
包含当前时间的对象来调度UI更新。感谢setState()
电话,React知道状态已经改变,并render()
再次调用该方法来了解屏幕上应显示的内容。这一次,this.state.date
该render()
方法将会不同,因此渲染输出将包含更新的时间。React会相应地更新DOM。 -
如果
Clock
组件从DOM中移除,React将调用componentWillUnmount()
生命周期钩子,以便定时器停止。
1.4、正确使用State
关于setState()需要知道3点
1、不要直接修改状态
// Wrong 不会重新渲染组件: this.state.comment = 'Hello'; // Correct 使用setState(): this.setState({comment: 'Hello'});
唯一可以分配的地方this.state
是构造函数。
2、状态更新可能是异步的
React可能会将多个setState()
调用分批到单个更新中进行性能调优。
因为this.props
和this.state
可异步更新,你不应该依赖于它们的值来计算下一个状态。
例如,此代码可能无法更新计数器:
// Wrong this.setState({ counter: this.state.counter + this.props.increment, });
要修复它,请使用setState()
接受函数而不是对象的第二种形式。该函数将接收前一个状态作为第一个参数,并将更新应用时的道具作为第二个参数:
// Correct this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));
我们使用了上面的箭头函数,但它也适用于常规函数:
// Correct this.setState(function(prevState, props) { return { counter: prevState.counter + props.increment }; });
3、状态更新已合并
当你调用setState()时
,React将你提供的对象合并到当前状态。
例如,您的状态可能包含多个独立变量:
constructor(props) { super(props); this.state = { posts: [], comments: [] }; }
然后,您可以使用单独的setState()
调用独立更新它们:
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
。
1.5、数据向下流动
不论是父组件还是子组件都不知道某个组件是有状态的还是无状态的,它们不应该关心它是被定义为一个函数还是一个类。
这就是为什么状态通常被称为本地或封装。除了拥有和设置它的组件之外,其他任何组件都无法访问它。
组件可以选择将其状态作为道具传递给其子组件:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
这也适用于用户定义的组件:
<FormattedDate date={this.state.date} />
该FormattedDate
组件会收到date
它的props,并不知道它是来自Clock state
,Clock props
还是手工输入:
function FormattedDate(props) { return <h2>It is {props.date.toLocaleTimeString()}.</h2>; }
这通常称为“自顶向下”或“单向”数据流。任何状态总是由某个特定组件拥有,并且从该状态派生的任何数据或UI只能影响树中“在其下”的组件。
如果您将组件树想象为props的瀑布,则每个组件的状态就像是一个额外的水源,它在任意点加入它,但是也会流下来。
为了显示所有组件都是真正隔离的,我们可以创建一个App
呈现三个<Clock>
s 的组件:
function App() { return ( <div> <Clock /> <Clock /> <Clock /> </div> ); } ReactDOM.render( <App />, document.getElementById('root') );
每个Clock
设置自己的计时器并独立更新。
在React应用程序中,无论组件是有状态的还是无状态的,都被视为可能随时间而改变的组件的实现细节。您可以在有状态组件内使用无状态组件,反之亦然。