在这篇文章中,我会介绍 React 各个生命周期在实际项目中的应用场景,并给出代码示例。
React 生命周期图谱:https://projects.wojtekmaj.pl...
组件实例创建并插入到 DOM 中。这个阶段调用生命周期函数顺序依次是: coustructor()、static getDerivedStateFromProps()、render()、componentDidMount()。
组件的 state 或者 props 发生变化时会触发更新。这个阶段调用生命周期函数顺序依次是:static getDerivedStateFromProps()、shouldComponentUpdate()、render()、getSnapshotBeforeUpdate()、componentDidUpdate()。
组件从 DOM 中移除时,会调用 componentWillUnmount() 方法。
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:static getDerivedStateFromError()、componentDidCatch()。
React 组件在挂载之前,会调用它的构造函数。
class Test extends React.Component { constructor(props) { super(props); this.state = { counter: 0 }; this.handleClick = this.handleClick.bind(this); } handleClick() { const { counter } = this.state; this.setState({ counter: counter + 1 }); } render() { return ( <div> {this.state.counter} <button onClick={this.handleClick}>click</button> </div> ); } } ReactDOM.render( <Test />, document.getElementById('root') );
React 组件挂载后(插入DOM 树中)立即调用。
在此处如果调用 setState(),将会触发渲染,这个渲染将会发生在浏览器更新屏幕之前,因此用户不会有感知,但是要谨慎使用该模式,因为会导致性能问题。 如果你的渲染依赖于 DOM 节点的位置和大小,比如 Modal 和 Tooltip ,则可以使用该模式。
React 组件更新后立即调用,首次渲染不会调用此方法。
componentDidUpdate(prevProps) { // 典型用法(不要忘记比较 props): if (this.props.id !== prevProps.id) { this.fetchData(this.props.id); } }
React 组件卸载或销毁之前调用
在该生命周期中不要调用 setState(),因为组件卸载后,不会重新渲染。
当 state 或者 props 发生变化时,shouldComponentUpdate() 将会在 render() 之前调用,首次渲染或者使用 forceUpdate() 时不会调用。
可以通过手写覆盖 shouldComponentUpdate() 方法对 React 组件进行性能优化,但是大部分情况下,可以通过继承 React.pureComponent 代替手写 shouldComponentUpdate(),React.pureComponent 实现了 shouldComponentUpdate() 方法(用当前和之前的 props 和 state 进行浅比较)。
class CounterButton extends React.Component { constructor(props) { super(props); this.state = {count: 1}; } shouldComponentUpdate(nextProps, nextState) { if (this.props.color !== nextProps.color) { return true; } if (this.state.count !== nextState.count) { return true; } return false; } render() { return ( <button color={this.props.color} onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button> ); } }
render() 方法之前调用,在初始挂载和后续更新都会调用。
适用于罕用例,即 state 的值在任何时候都取决于 props。
// 例子1: class Example extends React.Component { state = { isScrollingDown: false, lastRow: null }; static getDerivedStateFromProps(props, state) { if (props.currentRow !== state.lastRow) { return { isScrollingDown: props.currentRow > state.lastRow, lastRow: props.currentRow }; } // 返回 null 表示无需更新 state。 return null; } }
// 例子2:根据 props 获取 externalData class ExampleComponent extends React.Component { state = { externalData: null, }; static getDerivedStateFromProps(props, state) { // 保存 prevId 在 state 中,以便我们在 props 变化时进行对比。 // 清除之前加载的数据(这样我们就不会渲染旧的内容)。 if (props.id !== state.prevId) { return { externalData: null, prevId: props.id }; } // 无需更新 state return null; } componentDidMount() { this._loadAsyncData(this.props.id); } componentDidUpdate(prevProps, prevState) { if (this.state.externalData === null) { this._loadAsyncData(this.props.id); } } componentWillUnmount() { if (this._asyncRequest) { this._asyncRequest.cancel(); } } render() { if (this.state.externalData === null) { // 渲染加载状态 ... } else { // 渲染真实 UI ... } } _loadAsyncData(id) { this._asyncRequest = loadMyAsyncData(id).then( externalData => { this._asyncRequest = null; this.setState({externalData}); } ); } }
派生状态会导致代码冗余,并使组件难以维护,可以使用如下替换方案:
(1)memoization 模式:如果只是为了缓存基于当前 props 计算之后的结果的话,没有必要使用 getDerivedStateFromProps(),因为管理 state 的复杂度会随着需要管理的属性的增多而越来越庞大,比如,如果我们想在组件 state 里添加第二个派生 state,那就需要写两份跟踪变化的逻辑。为了让代码变得简单和易于管理,可以尝试使用 memoization。
// ******************************************************* // 注意:这个例子不是建议的方法。 // 下面的例子才是建议的方法。 // ******************************************************* static getDerivedStateFromProps(props, state) { // 列表变化或者过滤文本变化时都重新过滤。 // 注意我们要存储 prevFilterText 和 prevPropsList 来检测变化。 if ( props.list !== state.prevPropsList || state.prevFilterText !== state.filterText ) { return { prevPropsList: props.list, prevFilterText: state.filterText, filteredList: props.list.filter(item => item.text.includes(state.filterText)) }; } return null; }
// 使用 memoization import memoize from "memoize-one"; class Example extends Component { // state 只需要保存当前的 filter 值: state = { filterText: "" }; // 在 list 或者 filter 变化时,重新运行 filter: filter = memoize( (list, filterText) => list.filter(item => item.text.includes(filterText)) ); handleChange = event => { this.setState({ filterText: event.target.value }); }; render() { // 计算最新的过滤后的 list。 // 如果和上次 render 参数一样,`memoize-one` 会重复使用上一次的值。 const filteredList = this.filter(this.props.list, this.state.filterText); return ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); } }
(2)使用完全受控的组件:
function EmailInput(props) { return <input onChange={props.onChange} value={props.email} />; }
(3)使用有 key 的非可控组件(当 key 变化的时候,React 就会创建一个新的而不是一个既有的组件),大部分时候,这是处理重置 state 的最好的办法。
class EmailInput extends Component { state = { email: this.props.defaultEmail }; handleChange = event => { this.setState({ email: event.target.value }); }; render() { return <input onChange={this.handleChange} value={this.state.email} />; } } <EmailInput defaultEmail={this.props.user.email} key={this.props.user.id} />
但是在某些情况下,key 可能不起作用,这时候可以使用 getDerivedStateFromProps() 来观察属性变化。
class EmailInput extends Component { state = { email: this.props.defaultEmail, prevPropsUserID: this.props.userID }; static getDerivedStateFromProps(props, state) { // 只要当前 user 变化, // 重置所有跟 user 相关的状态。 // 这个例子中,只有 email 和 user 相关。 if (props.userID !== state.prevPropsUserID) { return { prevPropsUserID: props.userID, email: props.defaultEmail }; } return null; } // ... }
在最近一次渲染输出(提交到 DOM 节点)之前调用。
组件在发生更改之前可以从 DOM 中获取一些信息,这个方法可能用在 UI 处理中,例如滚动位置。
class ScrollingList extends React.Component { constructor(props) { super(props); this.listRef = React.createRef(); } getSnapshotBeforeUpdate(prevProps, prevState) { // 我们是否在 list 中添加新的 items ? // 捕获滚动位置以便我们稍后调整滚动位置。 if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; return list.scrollHeight - list.scrollTop; } return null; } componentDidUpdate(prevProps, prevState, snapshot) { // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items, // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。 //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值) if (snapshot !== null) { const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; } } render() { return ( <div ref={this.listRef}>{/* ...contents... */}</div> ); } }
该方法在组件抛出错误时被调用。它将抛出的错误作为参数,并且返回一个值以更新 state。
显示降级 UI
该方法会在渲染阶段调用,因此不允许出现副作用,如遇此类情况,可以用 componentDidCatch()。
该方法在“提交”的阶段被调用。因此允许执行副作用。
用于记录错误,该方法的第二个参数包含有关引发组件错误的栈信息。
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { error: null, errorInfo: null }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染可以显示降级 UI return { error: true }; } componentDidCatch(error, errorInfo) { this.setState({ errorInfo }) } render() { if (this.state.errorInfo) { return ( <div> <h2>Something went wrong.</h2> <details style={{ whiteSpace: 'pre-wrap' }}> {this.state.error && this.state.error.toString()} <br /> {this.state.errorInfo.componentStack} </details> </div> ); } return this.props.children; } } class BuggyCounter extends React.Component { constructor(props) { super(props); this.state = { counter: 0 }; this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState(({counter}) => ({ counter: counter + 1 })); } render() { if (this.state.counter === 5) { // 此处渲染会产生错误 return [1,2,3]; } return <h1 onClick={this.handleClick}>{this.state.counter}</h1>; } } function App() { return ( <div> <p> <b> This is an example of error boundaries in React 16. <br /><br /> Click on the numbers to increase the counters. <br /> The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component. </b> </p> <hr /> <ErrorBoundary> <p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them. </p> <BuggyCounter /> <BuggyCounter /> </ErrorBoundary> <hr /> <p>These two counters are each inside of their own error boundary. So if one crashes, the other is not affected. </p> <ErrorBoundary> <BuggyCounter /> </ErrorBoundary> <ErrorBoundary> <BuggyCounter /> </ErrorBoundary> </div> ); }