Hooks 和 React 生命周期

1,491 阅读5分钟

一、Hooks 模拟 React 生命周期

  • constructor => 函数组件不需要构造函数,我们可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,也可以传一个函数给 useState
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}`;
}
  • shouldComponentUpdate => 可以用 React.memo 包裹一个组件来对它的 props 进行浅比较

这里也可以使用 useMemo 优化每一个节点。

const Button = React.memo((props) => {
  // 具体的组件
});
  • componentDidMount, componentDidUpdate => useLayoutEffect 与它们两的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect
// componentDidMount
useEffect(()=>{
  // ...
}, [])

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

二、react 生命周期

V16.3 之前

react

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

V16.4 之后

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

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

  • reconciliation:
    • componentWillMount
    • componentWillReceiveProps
    • shouldConmponentUpdate
    • componentWillUpdate
  • commit
    • componentDidMount
    • componentDidUpdate

其中,reconciliation 阶段是可以被打断的,所以 reconcilation 阶段执行的函数就会出现多次调用的情况,显然,这是不合理的。 所以 V16.3 引入了新的 API 来解决这个问题:

  • static getDerivedStateFromProps: 该函数在挂载阶段和组件更新阶段都会执行,即每次获取新的 props 或 state 之后都会被执行,在挂载阶段用来代替 componentWillMount;在组件更新阶段配合 componentDidUpdate,可以覆盖 componentWillReceiveProps 的所有用法。

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

  • getSnapshotBeforeUpdate: 该函数会在 render 之后, DOM 更新前被调用,用于读取最新的 DOM 数据。 返回一个值,作为 componentDidUpdate 的第三个参数;配合 componentDidUpdate, 可以覆盖componentWillUpdate 的所有用法。

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

react

三、多个组件的执行顺序

  1. 父子组件
  • 挂载阶段

    • 第一阶段,由父组件开始执行到自身的 render,解析其下有哪些子组件需要渲染,并对其中同步的子组件进行创建,按递归顺序挨个执行各个子组件至 render,生成到父子组件对应的 Virtual DOM 树,并 commit 到 DOM。

    • 第二阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件各自的 componentDidMount,最后触发父组件的。

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

所以执行顺序是:父组件 getDerivedStateFromProps —> 同步子组件 getDerivedStateFromProps —> 同步子组件 componentDidMount —> 父组件 componentDidMount —> 异步子组件 getDerivedStateFromProps —> 异步子组件 componentDidMount

  • 更新阶段

    • 第一阶段,由父组件开始,执行 getDerivedStateFromProps,shouldComponentUpdat

    更新到自身的 render,解析其下有哪些子组件需要渲染,并对 子组件 进行创建,按 递归顺序 挨个执行各个子组件至 render,生成到父子组件对应的 Virtual DOM 树,并与已有的 Virtual DOM 树 比较,计算出 Virtual DOM 真正变化的部分 ,并只针对该部分进行的原生DOM操作。

    • 第二阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件以下函数,最后触发父组件的。getSnapshotBeforeUpdate() componentDidUpdate()

所以执行顺序是:父组件 getDerivedStateFromProps —> 父组件 shouldComponentUpdate —> 子组件 getDerivedStateFromProps —> 子组件 shouldComponentUpdate —> 子组件 getSnapshotBeforeUpdate —> 父组件 getSnapshotBeforeUpdate —> 子组件 componentDidUpdate —> 父组件 componentDidUpdate

  • 卸载阶段

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

注意 :如果卸载旧组件的同时伴随有新组件的创建,新组件会先被创建并执行完 render,然后卸载不需要的旧组件,最后新组件执行挂载完成的回调。

  1. 兄弟组件
  • 挂载阶段

若是同步路由,它们的创建顺序和其在共同父组件中定义的先后顺序是 一致 的。

若是异步路由,它们的创建顺序和 js 加载完成的顺序一致。

  • 更新阶段、卸载阶段

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

所以,兄弟组件更新、卸载阶段,请参考 父子组件。