💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 状态和生命周期 思考前面章节中提到的时钟的例子。 迄今我们只了解了一种更新 UI 的方式。 我们通过调用 ReactDOM.render() 方法来更新渲染输出: ~~~ 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 中[打开查看](http://codepen.io/gaearon/pen/gwoJZk?editors=0010)。 在本节中,我们将会了解如何使 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); ~~~ 在 CodePen 中[打开查看它](http://codepen.io/gaearon/pen/dpdoYR?editors=0010)。 然而,它丢失了一个重要的需求:事实是, Clock 设置一个时钟并每秒更新 UI 应该是 Clock 的实现细节。 理想情况下,我们希望只编写一次,使 Clock 更新它自己: ~~~ ReactDOM.render( <Clock />, document.getElementById('root') ); ~~~ 要实现这点,我们需要添加 “state” 到 Clock 组件。 状态和 props 类似,但是它是私有的,并且被组件完全控制。 我们之前提到的,组件定义为类有一些额外的功能。就是局部状态:只有类组件可以用的特性。 ## 转换功能组件为类组件 可以通过五部转换一个像 Clock 这样的功能组件为类组件: 1. 创建一个继承 React.Component 类的 ES6 同名类 2. 添加一个空方法名为 render() 3. 把函数体移动到 render() 方法 4. 在 render() 方法中使用 this.props 替代 props 5. 删除保留的空函数声明 ~~~ class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div> ); } } ~~~ 在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/zKRGpo?editors=0010)。 Clock 现在被定义为一个 类组件 而不是功能组件。 这使我们可以使用如局部状态和生命周期钩子的额外功能。 ## 向一个类组件添加局部状态 在有许多组件的应用中,非常重要的一点是当组件被销毁的时候要释放它们使用的资源。 我们希望无论何时 Clock 被首次渲染到 DOM 时[设置一个时钟](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval)。这在 React 中被称为 “mounting”。 另外我们还希望当 Clock 生成的 DOM 被移除时[清除这个时钟](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval)。在 React 中这叫做“unmounting”。 我们可以在组件类上声明特定的方法,当组件 mounts 或者 unmouts 时运行一些代码。 ~~~ 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.props 被 React 本身设置,this.state 有一个特定的意义,如果你需要保存一些不是用于视觉输出的内容,你可以方便的手动添加额外的字段到类中。 如果你不在 render() 中使用什么,它不应该出现在 state 中。 我们将在 componentWillUnmount() 生命周期钩子中拆除时钟: ~~~ componentWillUnmount() { clearInterval(this.timerID); } ~~~ 最终,我们将会实现每秒运行的 tick() 方法。 它将使用 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') ); ~~~ 在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/amqdNA?editors=0010)。 现在 Clock 的 tick() 将在每秒运行。 让我们快速回顾以下其中的过程,和方法被调用的顺序: 1. 当 `<Clock />` 被传递到 ReactDOM.render(), React 调用 Clock 组件的构造函数。由于 Clock 需要显示当前时间,它使用一个包含当前时间的对象初始化了 this.state。我们之后会更新这个状态。 2. React 之后会调用 Clock 组件的 render() 方法。这是 React 知道该显示什么到屏幕的原因。React 之后匹配 Clock 的 render 输出的内容来更新 DOM。 3. 当 Clock 输出被插入到 DOM, React 调用 componentDidMount() 生命周期钩子。其中,Clock 组件要求浏览器设置一个计时器来在每秒调用一次 tick() 。 4. 浏览器每秒都会调用 tick() 方法。在这里面, Clock 组件通过调用 setState() 并传递一个包含当前时间的对象来安排一个 UI 的更新。得益于 setState() 的调用,React 知道状态被改变了,然后再次调用 render() 方法来了解什么应该显示在屏幕中。这次,在render() 方法中的 this.state.date 将是不同的,所以 render 输出中会包含更新的时间。React 对 DOM 进行相应的更新。 5. 如果 Clock 组件被从 DOM 中移除,React 调用 componentWillUnmount() 生命周期钩子,所以计时器也会被停止。 ## 正确的使用状态 关于 setState() 有三件事是你应该知道的。 ### 1.不要直接修改 state 例如,这将导致不能重新渲染组件: ~~~ // 错误用法 this.state.comment = 'Hello'; ~~~ 而是使用 setState() 替代: ~~~ // 正确用法 this.setState({comment: 'Hello'}); ~~~ 赋值 this.state 只有一个正确的地点,就是 constructor 中。 ### 2. 状态更新可能是异步的 React 可能为了改进性能而批次处理多个 setState() 到一次更新。 因为 this.props 和 this.state 可能是异步更新的,你不能依赖他们的值计算下一个状态。 例如,这段代码可能导致更新 counter 失败: ~~~ // 错误 this.setState({ counter: this.state.counter + this.props.increment, }); ~~~ 要弥补这个问题,使用另一种 setState() 的形式,它接受一个函数而不是一个对象。这个函数将接收前一个状态作为第一个参数,应用更新时的 props 作为第二个参数: ~~~ // 正确 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); ~~~ 我们在上面使用了一个[箭头函数](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions),但是也可以使用一个常规的函数: ~~~ // 正确 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 。 ## 数据流向 父组件和子组件都不能知道是否某个组件是有状态的或无状态的,它们也不应该在意它是被定义为一个功能组件还是一个类组件。 这是 state 经常称为局部或者封装的原因。它不能被除了拥有并设置它的另外的任何组件访问。 一个组件可以选择向下传递它的状态作为它的子组件的 props : ~~~ <h2>It is {this.state.date.toLocaleTimeString()}.</h2> ~~~ 对于用户定义的组件也同样: ~~~ <FormattedDate date={this.state.date} /> ~~~ FormattedDate 组件可以接受它的 props 中的 date ,并不能知道它是否来自 Clock 的 state、props 或者是手动创建: ~~~ function FormattedDate(props) { return <h2>It is {props.date.toLocaleTimeString()}.</h2>; } ~~~ 在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/zKRqNB?editors=0010)。 这通常称为一个“从上到下”或者“单向”的数据流。任何状态总是被某个特定的组件所有,任何被这个状态驱动的数据或者 UI 都只影响树中“下方”的组件。 如果你设想一个组件树作为一个瀑布式的 props,每个组件的状态都像一个额外的水源,然后在任意点汇入它,但是同样只能向下流。 要展示这个,所有组件都是完全独立的,我们一个 App 组件来渲染三个 `<Clock>`: ~~~ function App() { return ( <div> <Clock /> <Clock /> <Clock /> </div> ); } ReactDOM.render( <App />, document.getElementById('root') ); ~~~ 在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/vXdGmd?editors=0010)。 每个 Clock 都设置它自己的计时器并独立更新。 在 React App 中,一个组件是否是有状态或者无状态的,被认为是组件的一个实现细节,随着时间推移可能发生改变。你可以在有状态的组件中使用无状态组件,反之亦然。