面试题:Hooks 与 React 生命周期的关系

你真的了解 React 生命周期吗?

React 生命周期不少人都了解,但一般咱们所了解的都是 单个组件 的生命周期,但针对 Hooks 组件、多个关联组件(父子组件和兄弟组件) 的生命周期又是怎么样的喃?你有思考和了解过吗,接下来咱们将完整的了解 React 生命周期。react

关于 组件 ,咱们这里指的是 React.Component 以及 React.PureComponent ,可是否包括 Hooks 组件喃?算法

1、Hooks 组件

函数组件 的本质是函数,没有 state 的概念的,所以不存在生命周期一说,仅仅是一个 render 函数而已。数组

可是引入 Hooks 以后就变得不一样了,它能让组件在不使用 class 的状况下拥有 state,因此就有了生命周期的概念,所谓的生命周期其实就是 useStateuseEffect()useLayoutEffect()浏览器

即:Hooks 组件(使用了Hooks的函数组件)有生命周期,而函数组件(未使用Hooks的函数组件)是没有生命周期的安全

下面,是具体的 class 与 Hooks 的生命周期对应关系app

  • constructor:函数组件不须要构造函数,咱们能够经过调用 useState 来初始化 state。若是计算的代价比较昂贵,也能够传一个函数给 useStatedom

    const [num, UpdateNum] = useState(0)
  • getDerivedStateFromProps:通常状况下,咱们不须要使用它,咱们能够在渲染过程当中更新 state,以达到实现 getDerivedStateFromProps 的目的。异步

    function ScrollView({row}) {
      let [isScrollingDown, setIsScrollingDown] = useState(false);
      let [prevRow, setPrevRow] = useState(null);
    
      if (row !== prevRow) {
        // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
        setIsScrollingDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
      }
    
      return `Scrolling down: ${isScrollingDown}`;
    }

    React 会当即退出第一次渲染并用更新后的 state 从新运行组件以免耗费太多性能。函数

  • shouldComponentUpdate:能够用 React.memo 包裹一个组件来对它的 props 进行浅比较工具

    const Button = React.memo((props) => {
      // 具体的组件
    });

    注意:React.memo 等效于 PureComponent,它只浅比较 props。这里也可使用 useMemo 优化每个节点。

  • render:这是函数组件体自己。
  • componentDidMount, componentDidUpdate: useLayoutEffect 与它们两的调用阶段是同样的。可是,咱们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffectuseEffect 能够表达全部这些的组合。

    // componentDidMount
    useEffect(()=>{
      // 须要在 componentDidMount 执行的内容
    }, [])
    
    useEffect(() => { 
      // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
      document.title = `You clicked ${count} times`; 
      return () => {
        // 须要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵照先清理后更新)
        // 以及 componentWillUnmount 执行的内容       
      } // 当函数中 Cleanup 函数会按照在代码中定义的顺序前后执行,与函数自己的特性无关
    }, [count]); // 仅在 count 更改时更新

    请记得 React 会等待浏览器完成画面渲染以后才会延迟调用 useEffect,所以会使得额外操做很方便

  • componentWillUnmount:至关于 useEffect 里面返回的 cleanup 函数

    // componentDidMount/componentWillUnmount
    useEffect(()=>{
      // 须要在 componentDidMount 执行的内容
      return function cleanup() {
        // 须要在 componentWillUnmount 执行的内容      
      }
    }, [])
  • componentDidCatch and getDerivedStateFromError:目前尚未这些方法的 Hook 等价写法,但很快会加上。

为方便记忆,大体汇总成表格以下。

class 组件 Hooks 组件
constructor useState
getDerivedStateFromProps useState 里面 update 函数
shouldComponentUpdate useMemo
render 函数自己
componentDidMount useEffect
componentDidUpdate useEffect
componentWillUnmount useEffect 里面返回的函数
componentDidCatch
getDerivedStateFromError

2、单个组件的生命周期

1. 生命周期

V16.3 以前

咱们能够将生命周期分为三个阶段:

  • 挂载阶段
  • 组件更新阶段
  • 卸载阶段

分开来说:

  1. 挂载阶段

    • constructor:避免将 props 的值复制给 state
    • componentWillMount
    • render:react 最重要的步骤,建立虚拟 dom,进行 diff 算法,更新 dom 树都在此进行
    • componentDidMount
  2. 组件更新阶段

    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  3. 卸载阶段

    • componentWillUnMount

这种生命周期会存在一个问题,那就是当更新复杂组件的最上层组件时,调用栈会很长,若是在进行复杂的操做时,就可能长时间阻塞主线程,带来很差的用户体验,Fiber 就是为了解决该问题而生。

V16.3 以后

Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将以前的同步渲染改为了异步渲染,在不影响体验的状况下去分段计算更新。

对于异步渲染,分为两阶段:

  • reconciliation

    • componentWillMount
    • componentWillReceiveProps
    • shouldConmponentUpdate
    • componentWillUpdate
  • commit

    • componentDidMount
    • componentDidUpdate

其中,reconciliation 阶段是能够被打断的,因此 reconcilation 阶段执行的函数就会出现屡次调用的状况,显然,这是不合理的。

因此 V16.3 引入了新的 API 来解决这个问题:

  1. static getDerivedStateFromProps: 该函数在挂载阶段和组件更新阶段都会执行,即每次获取新的propsstate 以后都会被执行在挂载阶段用来代替componentWillMount;在组件更新阶段配合 componentDidUpdate,能够覆盖 componentWillReceiveProps 的全部用法。

    同时它是一个静态函数,因此函数体内不能访问 this,会根据 nextPropsprevState 计算出预期的状态改变,返回结果会被送给 setState返回 null 则说明不须要更新 state,而且这个返回是必须的

  2. getSnapshotBeforeUpdate: 该函数会在 render 以后, DOM 更新前被调用,用于读取最新的 DOM 数据。

    返回一个值,做为 componentDidUpdate 的第三个参数;配合 componentDidUpdate, 能够覆盖componentWillUpdate 的全部用法。

注意:V16.3 中只用在组件挂载或组件 props 更新过程才会调用,即若是是由于自身 setState 引起或者forceUpdate 引起,而不是由父组件引起的话,那么static getDerivedStateFromProps也不会被调用,在 V16.4 中更正为都调用。

即更新后的生命周期为:

  1. 挂载阶段

    • constructor
    • static getDerivedStateFromProps
    • render
    • componentDidMount
  2. 更新阶段

    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  3. 卸载阶段

    • componentWillUnmount

2. 生命周期,误区

误解一:getDerivedStateFromProps 和 componentWillReceiveProps 只会在 props 改变 时才会调用

实际上,只要父级从新渲染,getDerivedStateFromProps 和 componentWillReceiveProps 都会从新调用,无论 props 有没有变化。因此,在这两个方法内直接将 props 赋值到 state 是不安全的。

// 子组件
class PhoneInput extends Component {
  state = { phone: this.props.phone };

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

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

  componentWillReceiveProps(nextProps) {
    // 不要这样作。
    // 这会覆盖掉以前全部的组件内 state 更新!
    this.setState({ phone: nextProps.phone });
  }
}

// 父组件
class App extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    // 使用了 setInterval,
    // 每秒钟都会更新一下 state.count
    // 这将致使 App 每秒钟从新渲染一次
    this.interval = setInterval(
      () =>
        this.setState(prevState => ({
          count: prevState.count + 1
        })),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return (
      <>
        <p>
          Start editing to see some magic happen :)
        </p>
        <PhoneInput phone='call me!' /> 
        <p>
          This component will re-render every second. Each time it renders, the
          text you type will be reset. This illustrates a derived state
          anti-pattern.
        </p>
      </>
    );
  }
}

实例可点击这里查看

固然,咱们能够在 父组件App 中 shouldComponentUpdate 比较 props 的 email 是否是修改再决定要不要从新渲染,可是若是子组件接受多个 props(较为复杂),就很难处理,并且 shouldComponentUpdate 主要是用来性能提高的,不推荐开发者操做 shouldComponetUpdate(可使用 React.PureComponet)。

咱们也可使用 在 props 变化后修改 state

class PhoneInput extends Component {
  state = {
    phone: this.props.phone
  };

  componentWillReceiveProps(nextProps) {
    // 只要 props.phone 改变,就改变 state
    if (nextProps.phone !== this.props.phone) {
      this.setState({
        phone: nextProps.phone
      });
    }
  }
  
  // ...
}

但这种也会致使一个问题,当 props 较为复杂时,props 与 state 的关系很差控制,可能致使问题

解决方案一:彻底可控的组件

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

彻底由 props 控制,不派生 state

解决方案二:有 key 的非可控组件

class PhoneInput extends Component {
  state = { phone: this.props.defaultPhone };

  handleChange = event => {
    this.setState({ phone: event.target.value });
  };

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

<PhoneInput
  defaultPhone={this.props.user.phone}
  key={this.props.user.id}
/>

当 key 变化时, React 会建立一个新的而不是更新一个既有的组件

误解二:将 props 的值直接复制给 state

应避免将 props 的值复制给 state

constructor(props) {
 super(props);
 // 千万不要这样作
 // 直接用 props,保证单一数据源
 this.state = { phone: props.phone };
}

3、多个组件的执行顺序

1. 父子组件

  • 挂载阶段

    两个 阶段:

    • 阶段,由父组件开始执行到自身的 render,解析其下有哪些子组件须要渲染,并对其中 同步的子组件 进行建立,按 递归顺序 挨个执行各个子组件至 render,生成到父子组件对应的 Virtual DOM 树,并 commit 到 DOM。
    • 阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件各自的 componentDidMount,最后触发父组件的。

注意:若是父组件中包含异步子组件,则会在父组件挂载完成后被建立。

因此执行顺序是:

父组件 getDerivedStateFromProps —> 同步子组件 getDerivedStateFromProps —> 同步子组件 componentDidMount —> 父组件 componentDidMount —> 异步子组件 getDerivedStateFromProps —> 异步子组件 componentDidMount

  • 更新阶段

    React 的设计遵循单向数据流模型 ,也就是说,数据均是由父组件流向子组件。

    • 阶段,由父组件开始,执行

      1. static getDerivedStateFromProps
      2. shouldComponentUpdate
更新到自身的 `render`,解析其下有哪些子组件须要渲染,并对 **子组件** 进行建立,按 **递归顺序** 挨个执行各个子组件至 `render`,生成到父子组件对应的 Virtual DOM 树,并与已有的 Virtual DOM 树 比较,计算出 **Virtual DOM 真正变化的部分** ,并只针对该部分进行的原生DOM操做。
  • 阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件如下函数,最后触发父组件的。

    1. getSnapshotBeforeUpdate()
    2. componentDidUpdate()
React 会按照上面的顺序依次执行这些函数,每一个函数都是各个子组件的先执行,而后才是父组件的执行。

因此执行顺序是:

父组件 getDerivedStateFromProps —> 父组件 shouldComponentUpdate —> 子组件 getDerivedStateFromProps —> 子组件 shouldComponentUpdate —> 子组件 getSnapshotBeforeUpdate —>  父组件 getSnapshotBeforeUpdate —> 子组件 componentDidUpdate —> 父组件 componentDidUpdate
  • 卸载阶段

    componentWillUnmount(),顺序为 父组件的先执行,子组件按照在 JSX 中定义的顺序依次执行各自的方法

    注意 :若是卸载旧组件的同时伴随有新组件的建立,新组件会先被建立并执行完 render,而后卸载不须要的旧组件,最后新组件执行挂载完成的回调。

2. 兄弟组件

  • 挂载阶段

    如果同步路由,它们的建立顺序和其在共同父组件中定义的前后顺序是 一致 的。

    如果异步路由,它们的建立顺序和 js 加载完成的顺序一致。

  • 更新阶段、卸载阶段

    兄弟节点之间的通讯主要是通过父组件(Redux 和 Context 也是经过改变父组件传递下来的 props 实现的),知足React 的设计遵循单向数据流模型所以任何两个组件之间的通讯,本质上均可以归结为父子组件更新的状况

    因此,兄弟组件更新、卸载阶段,请参考 父子组件

走在最后:走心推荐一个在线编辑工具:StackBlitz,能够在线编辑 Angular、React、TypeScript、RxJS、Ionic、Svelte项目

预告:后续将加入高阶组件的生命周期,敬请期待小瓶子的下次更新。

相关文章
相关标签/搜索