• 重新学习react生命周期


    重新学习 React (一) 生命周期,Fiber 调度和更新机制

    前几天面试问道 react 的相关知识,对我打击比较大,感觉对 react 认识非常肤浅,所以在这里重新梳理一下,想想之前没有仔细思考过的东西。

    另外有说的不对的地方还请帮我指正一下,先谢谢各位啦。

    目录索引:

    什么是生命周期和调度?

    React 有一套合理的运行机制去控制程序在指定的时刻该做什么事,当一个生命周期钩子被触发后,紧接着会有下一个钩子,直到整个生命周期结束。

    生命周期

    生命周期代表着每个执行阶段,比如组件初始化,更新完成,马上要卸载等等,React 会在指定的时机执行相关的生命周期钩子,使我们可以有机在程序运行中会插入自己的逻辑。

    调度

    我们写代码的时候往往会有很多组件以及他们的子组件,各自调用不同的生命周期,这时就要解决谁先谁后的问题,在 react v16 之前是采用了递归调用的方式一个一个执行,而在现在 v16 的版本中则采用了与之完全不同的处理(调度)方式,名叫 Fiber,这个东西 facebook 做了有两年时间,实现非常复杂。

    具体 Fiber 它是一个什么东西呢?不要着急,我们先从最基本的生命周期钩子看起。

    React 生命周期详解

    首先看一下 React V16.4 后的生命周期概况(图片来源

    • 从横向看,react 分为三个阶段:
      • 创建时
        • constructor() - 类构造器初始化
        • static getDerivedStateFromProps() - 组件初始化时主动触发
        • render() - 递归生成虚拟 DOM
        • componentDidMount() - 完成首次 DOM 渲染
      • 更新时
        • static getDerivedStateFromProps() - 每次 render() 之前执行
        • shouldComponentUpdate() - 校验是否需要执行更新操作
        • render() - 递归生成虚拟 DOM
        • getSnapshotBeforeUpdate() - 在渲染真实 DOM 之前
        • componentDidUpdate() - 完成 DOM 渲染
      • 卸载时
        • componentWillUnmount() - 组件销毁之前被直接调用

    一些干货

    • 有三种方式可以触发 React 更新,props 发生改变,调用 setState() 和调用 forceUpdate()
    • static getDerivedStateFromProps() 这个钩子会在每个更新操作之前(即使props没有改变)执行一次,使用时应该保持谨慎。
    • componentDidMount() 和 componentDidUpdate() 执行的时机是差不多的,都在 render 之后,只不过前者只在首次渲染后执行,后者首次渲染不会执行
    • getSnapshotBeforeUpdate() 执行时可以获得只读的新 DOM 树,此函数的返回值为 componentDidUpdate(prevProps, prevState, snapshot) 的第三个参数

    尝试理解 Fiber

    关于 Fiber,强烈建议听一下知乎上程墨Morgan的 live 《深入理解React v16 新功能》,这里潜水员的例子和图片也是引用于此 live。

    背景

    我们知道 React 是通过递归的方式来渲染组件的,在 V16 版本之前的版本里,当一个状态发生变更时,react 会从当前组件开始,依次递归调用所有的子组件生命周期钩子,而且这个过程是同步执行的且无法中断的,一旦有很深很深的组件嵌套,就会造成严重的页面卡顿,影响用户体验。

    React 在V16版本之前的版本里引入了 Fiber 这样一个东西,它的英文涵义为纤维,在计算机领域它排在在进程和线程的后面,虽然 React 的 Fiber 和计算机调度里的概念不一样,但是可以方便对比理解,我们大概可以想象到 Fiber 可能是一个比线程还短的时间片段。

    Fiber 到底做了什么事

    Fiber 把当前需要执行的任务分成一个个微任务,安排优先级,然后依次处理,每过一段时间(非常短,毫秒级)就会暂停当前的任务,查看有没有优先级较高的任务,然后暂停(也可能会完全放弃)掉之前的执行结果,跳出到下一个微任务。同时 Fiber 还做了一些优化,可以保持住之前运行的结果以到达复用目的。

    举个潜水员的例子

    我们可以把调度当成一个潜水员在海底寻宝,v16 之前是通过组件递归的方式进行寻宝,从父组件开始一层一层深入到最里面的子组件,也就是如下图所示。

    而替换成了 Fiber 后,海底变成的狭缝(简单理解为递归变成了遍历),潜水员会每隔一小段时间浮出水面,看看有没有其他寻宝任务。注意此时没有寻到宝藏的话,那么之前潜水的时间就浪费了。就这样潜水员会一直下潜和冒泡,具体如下图所示。

    引入 Fiber 后带来的三个阶段

    从生命周期那张图片纵向来看,Fiber 将整个生命周期分成了三个阶段:

    • render 阶段
      • 由于 Fiber 会时不时跳出任务,然后重新执行,会导致该阶段的生命周期调用多次的现象,所以 React V16 之前 componentWillMount()componentWillUpdate()componentWillReceiveProps() 的三个生命周期钩子被加上了 UNSAFE 标记
      • 这个阶段效率不一定会比之前同步递归来的快,因为会有任务跳出重做的性能损耗,但是从宏观上看,它不断执行了最高优先级(影响用户使用体验)的任务,所以用户使用起来会比以前更加的流畅
      • 这个阶段的生命周期钩子可能会重复调用,建议只写无副作用的代码
    • pre-commit 阶段
      • 该阶段 DOM 已经形成,但还是只读状态
      • 这个阶段组件状态不会再改变
    • commit 阶段
      • 此时的 DOM 可以进行操作
      • 这个阶段组件已经完成更新,可以写一些有副作用的代码和添加其它更新操作。

    简而言之:以 render() 为界,之前执行的生命周期都有可能会打断并多次调用,之后的生命周期是不可被打断的且只会调用一次。所以尽量把副作用的代码放在只会执行一次的 commit 阶段。

    其它生命周期钩子

    除了上面常用的钩子,React 还提供了如下钩子:

    • static getDerivedStateFromError() 在 render 阶段执行,通过返回 state 更新组件状态
    • componentDidCatch() 在 commit 阶段执行,可以放一些有副作用的代码

    更新机制

    理解了生命周期和三个执行阶段,就可以比较容易理解组件状态的更新机制了。

    setState()

    这个方法可以让我们更新组件的 state 状态。第一个参数可以是对象,也可以是 updater 函数,如果是函数,则会接受当前的 state 和 props 作为参数。第二个参数为函数,是在 commit 阶段后执行,准确的说是在 componentDidUpdate() 后执行。

    setState() 的更新过程是异步的(除非绑定在 DOM 事件中或写在 setTimeout 里),而且会在最后合并所有的更新,如下:

    1.  
      Object.assign(
    2.  
      previousState,
    3.  
      {quantity: state.quantity + 1},
    4.  
      {quantity: state.quantity + 1},
    5.  
      ...
    6.  
      )
    7.  
      复制代码

    之所以设计成这样,是为了避免在一次生命周期中出现多次的重渲染,影响页面性能。

    forceUpdate()

    如果我们想强制刷新一个组件,可以直接调用该方法,调用时会直接执行 render() 这个函数而跳过 shouldComponentUpdate()

    举个极端例子

    1.  
      function wait() {
    2.  
      return new Promise(resolve => {
    3.  
      setTimeout(() => {
    4.  
      resolve();
    5.  
      console.log("wait");
    6.  
      }, 0);
    7.  
      });
    8.  
      }
    9.  
       
    10.  
      //......省略组件创建
    11.  
      async componentDidMount() {
    12.  
      await wait();
    13.  
      this.setState({
    14.  
      name: "new name"
    15.  
      });
    16.  
      console.log("componentDidMount");
    17.  
      }
    18.  
       
    19.  
      componentDidUpdate() {
    20.  
      console.log("componentDidUpdate");
    21.  
      }
    22.  
       
    23.  
      render() {
    24.  
      console.log(this.state);
    25.  
      return null
    26.  
      }
    27.  
      //......省略组件创建
    28.  
       
    29.  
      // 输出结果如下
    30.  
      // wait
    31.  
      // {name: "new name"}
    32.  
      // componentDidUpdate
    33.  
      // componentDidMount
    34.  
       
    35.  
      // 注意 componentDidUpdate 的输出位置,一般情况下
    36.  
      // componentDidUpdate 都是在componentDidMount 后面
    37.  
      // 执行的,但是这里因为setState 写在了 await 后面
    38.  
      // 所以情况相反。
    39.  
      复制代码

    结语

    了解 react 生命周期和更新机制确实有利于编写代码,特别是当代码量越来越大时,错用的 setState 或生命周期钩子都可能埋下越来越多的雷,直到有一天无法维护。。。

    我的个人建议如下:

    • 把副作用代码通通放在 commit 阶段,因为这个阶段不会影响页面渲染性能
    • 尽可能不要使用 forceUpdate() 方法,借用 Evan You 的一句话,如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事
    • 只要调用了 setState() 就会进行 render(),无论 state 是否改变
    • 知道 setState() 更新的什么时候是同步的,什么时候是异步的,参见上文
    • 不要把 getDerivedStateFromProps() 当成是 UNSAFE_componentWillReceiveProps() 的替代品,因为 getDerivedStateFromProps() 会在每次 render() 之前执行,即使 props 没有改变


    作者:saiyan
    链接:https://juejin.im/post/5cf34ef66fb9a07ee5660735
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    SPA项目开发之动态树以及数据表格和分页
    SPA项目开发之首页导航左侧菜单栏
    SPA项目开发之登录
    使用vue-cli搭建spa项目
    Splay 平衡树
    主席树(可持久化线段树 )
    P3195 [HNOI2008]玩具装箱TOY
    P2962 [USACO09NOV]灯Lights
    【hdu4405】AeroplaneChess
    HDU3853:LOOPS
  • 原文地址:https://www.cnblogs.com/fs0196/p/12964836.html
Copyright © 2020-2023  润新知