React 生命周期

14 5月

React16生命周期有点不同,可以点这里

  • 时序图
  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate
  • getDerivedStateFromError,componentDidCatch
  • 废弃的生命周期

时序图

Mounting装载阶段顺序:

  • constructor()
  • static getDerivedStateFromProps(props, state)
  • render()
  • componentDidMount()

Updating更新阶段顺序:

  • static getDerivedStateFromProps(props, state)
  • shouldComponentUpdate(nextProps, nextState)
  • render()
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • componentDidUpdate(prevProps, prevState, snapshot)

Unmounting卸载阶段顺序:

  • componentWillUnmount()

getDerivedStateFromProps

static getDerivedStateFromProps(props, state):在render之前被调用。方法名起的不错,非常语义化,就是根据props的变化去更新state。返回一个对象去更新state,如果不需要更新state就返回null。

React16之前用componentWillReceiveProps,通常都是父组件重新渲染时,子组件比较新旧props,不同的话,将新props的值更新到state里,触发render。最好先看一下这篇文章:你可能并不需要派生状态

首先要反思一下为何需要componentWillReceiveProps?组件内部状态,最好有单一来源,要么来自props,要么来自state,但有的组件并不是这样设计的,例如下面这个 email input的例子:

import React, { Component } from 'react';

class EmailInput extends Component {
    state = { email: this.props.email };

    componentWillReceiveProps(nextProps) {
        this.setState({ email: nextProps.email });    // 不要这么做
    }

    handleChange = e => {
        this.setState({ email: e.target.value });
    };

    render() {
        return <input onChange={this.handleChange} value={this.state.email} />;
    }
}

上面这个例子,state由props初始化,并在输入时实时更新state。组件既受控又非受控,但其实有bug,如果父组件重新渲染,我们输入的任何东西都将丢失,点这里

你可能会在componentWillReceiveProps里加限制,只有props里的email发生变化才更新state。但仍旧会有bug,例如有“重置”按钮时,input框不会回到初始状态,点这里

componentWillReceiveProps(nextProps) {
    if (nextProps.email !== this.props.email) {
        this.setState({
            email: nextProps.email
        });
    }
}

避免这种问题,首选方案就是设计成受控组件:

function EmailInput(props) {
    return  <input onChange={props.onChange} value={props.email} />;
}

次选方案是带有key属性的非受控组件,点这里

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属性用于重置
/>

key是React的特殊属性。当key发生变化时,React将创建一个新的组件实例,而不是更新当前的一个实例。在大多数情况下,这是处理有重置要求的状态的最好方法。

用key会导致每次都创建一个新的组件,而不是新旧组件diff,通常这种性能开销是可以忽略不计的。但如果初始化的代价非常昂贵,不想设计成带key的非受控组件,此时就可以用getDerivedStateFromProps。(还能有选择地重置某些state)

class EmailInput extends Component {
    state = {
        email: this.props.defaultEmail,
        prevPropsUserID: this.props.userID
    };

    static getDerivedStateFromProps(nextProps, prevState) {
        if (nextProps.userID !== prevState.prevPropsUserID) {
            return {
                email: nextProps.defaultEmail,
                prevPropsUserID: nextProps.userID
            };
        }
        return null;
    }
    ...
}

回顾一下,你真的需要使用getDerivedStateFromProps吗?尽量避免设计成既受控又非受控,推荐受控。但现实情况是组件通常包含受控和非受控的混合行为,可以设计成带key的非受控,最后再考虑使用getDerivedStateFromProps。

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState):在DOM变化前捕获一些信息(例如鼠标滚动位置),返回值将被传给componentDidUpdate(prevProps, prevState, snapshot)。

也不常用,例如列表有新选项时,自动滚动到新选项的位置:

import React, { Component } from 'react';

class ScrollingList extends Component {
    constructor(props) {
        super(props);
        this.listRef = React.createRef();
    }

    getSnapshotBeforeUpdate(prevProps, prevState) {
        if (prevProps.list.length < this.props.list.length) {
            const list = this.listRef.current;
            return list.scrollHeight - list.scrollTop;
        }
        return null;
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        if (snapshot !== null) {
            const list = this.listRef.current;
            list.scrollTop = list.scrollHeight - snapshot;
        }
    }

    render() {
        return <div ref={this.listRef}>...</div>;
    }
}

上面的例子中,用getSnapshotBeforeUpdate读取scrollHeight是很重要的,因为在React的“render”阶段和“commit”阶段之间可能存在延迟。

getDerivedStateFromError,componentDidCatch

类组件中定义了static getDerivedStateFromError(error),或者,componentDidCatch(error, info),它就成了异常边界组件(ErrorBoundary)。可以在子组件树的任何位置捕获JS错误,记录错误,而不是使整个组件树崩溃。工作方式类似于JavaScript的catch {} 块。

通常你会在整个应用程序中使用它:

import React, { Component } from 'react';

export default class ErrorBoundary extends Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }
  
    static getDerivedStateFromError(error) {
        return { hasError: true };
    }
  
    componentDidCatch(error, info) {
        // Example "componentStack":
        //   in ComponentThatThrows (created by App)
        //   in ErrorBoundary (created by App)
        //   in div (created by App)
        //   in App
        logComponentStackToMyService(info.componentStack);
    }
  
    render() {
        if (this.state.hasError) {
            return <h1>Something went wrong.</h1>;
        }
        return this.props.children; 
    }
}

ErrorBoundary弥补了try-catch仅能在命令式代码中使用的局限,因为React组件大多是声明式的。

ErrorBoundary无法捕捉:异步代码(例如setTimeout,requestAnimationFrame的回调函数),服务端渲染等。

废弃的生命周期

以下生命周期已被标记为unsafe,不建议使用:(Will基本一锅端,只剩WillUnmount瑟瑟发抖)

  • componentWillMount()
  • componentWillUpdate
  • componentWillReceiveProps

评论(2)

发表评论

电子邮件地址不会被公开。 必填项已用*标注