前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React 进阶 - lifecycle

React 进阶 - lifecycle

作者头像
Cellinlab
发布2023-05-17 20:55:49
8570
发布2023-05-17 20:55:49
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

# 生命周期

React 类组件为开发者提供了一些生命周期钩子函数,能让开发者在 React 执行的重要阶段,在钩子函数里做一些该做的事。自从 React Hooks 问世以来,函数组件也能优雅地使用 Hooks ,弥补函数组件没有生命周期的缺陷。

# 类组件生命周期

React 两个重要阶段,render 阶段和 commit 阶段,React 在调和( render )阶段会深度遍历 React fiber 树,目的就是发现不同( diff ),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行 render 函数。在一次调和过程完毕之后,就到了 commit 阶段,commit 阶段会创建修改真实的 DOM 节点。

如果在一次调和的过程中,发现了一个 fiber tag = 1 类组件的情况,就会按照类组件的逻辑来处理:

代码语言:javascript
复制
// react-reconciler\src\ReactFiberBeginWork.js
function updateClassComponent() {
  let shouldUpdate;
  const instance = workInProgress.stateNode; // stateNode 是 fiber 指向类组件实例的引用
  if (instance === null) { // 实例不存在,即该类组件没有被挂载过,那走初始化流程
    // 组件实例在这个方法中被创建
    contructorClassInstance(workInProgress, Component, nextProps);
    // 初始化挂载组件流程
    mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime);
    shouldUpdate = true; // 初始化阶段,肯定要更新
  } else {
    // 组件实例已经存在,那就是更新阶段
    shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderExpirationTime); // 更新组件流程
  }
  if (shouldUpdate) {
    nextChildren = instance.render(); // 执行 render 函数,获取组件的子节点
    reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime); // 调和子节点
  }
}

几个重要的概念:

  • instance 类组件对应的实例
  • workInProgress 树,当前正在调和的 fiber 树 ,一次更新中,React 会自上而下深度遍历子代 fiber ,如果遍历到一个 fiber ,会把当前 fiber 指向 workInProgress
  • current 树,在初始化更新中,current = null ,在第一次 fiber 调和之后,会将 workInProgress 树赋值给 current
    • React 来用 workInProgresscurrent 来确保一次更新中,快速构建,并且状态不丢失
  • Component 就是项目中的 class 组件
  • nextProps 作为组件在一次更新中新的 props
  • renderExpirationTime 作为下一次渲染的过期时间

在组件实例上可以通过 _reactInternals 属性来访问组件对应的 fiber 对象。在 fiber 对象上,可以通过 stateNode 来访问当前 fiber 对应的组件实例:

React 的大部分生命周期的执行,都在 mountClassInstanceupdateClassInstance 这两个方法中执行。

# 组件初始化阶段

  1. constructor 执行
    • mount 阶段,首先执行的 constructClassInstance 函数,用来实例化 React 组件,组件中 constructor 就是在这里执行的
    • 在实例化组件之后,会调用 mountClassInstance 组件初始化
  2. getDerivedStateFromProps 执行
    • 在初始化阶段,getDerivedStateFromProps 是第二个执行的生命周期,值得注意的是它是从 ctor 类上直接绑定的静态方法,传入 propsstate
    • 返回值将和之前的 state 合并,作为新的 state ,传递给组件实例使用
  3. componentWillMount 执行
    • 如果存在 getDerivedStateFromPropsgetSnapshotBeforeUpdate 就不会执行生命周期 componentWillMount
  4. render 函数执行
    • 到此为止 mountClassInstancec 函数完成,但是 updateClassComponent 函数, 在执行完 mountClassInstancec 后,执行了 render 渲染函数,形成了 children , 接下来 React 调用 reconcileChildren 方法深度调和 children
  5. componentDidMount 执行
    • 一旦 React 调和完所有的 fiber 节点,就会到 commit 阶段,在组件初始化 commit 阶段,会调用 componentDidMount 生命周期

执行顺序:constructor => getDerivedStateFromProps / componentWillMount => render => componentDidMount

# 组件更新

  1. componentWillReceiveProps
    • 首先判断 getDerivedStateFromProps 生命周期是否存在,如果不存在就执行 componentWillReceiveProps 生命周期
    • 传入该生命周期两个参数,分别是 newPropsnextContext
  2. getDerivedStateFromProps
    • 执行生命周期 getDerivedStateFromProps , 返回的值用于合并 state ,生成新的 state
  3. shouldComponentUpdate
    • 执行生命周期 shouldComponentUpdate ,传入新的 props ,新的 state ,和新的 context ,返回值决定是否继续执行 render 函数,调和子节点
    • 注意,getDerivedStateFromProps 的返回值可以作为新的 state ,传递给 shouldComponentUpdate
  4. componentWillUpdate
    • 执行生命周期 componentWillUpdateupdateClassInstance 方法到此执行完毕了
  5. render 函数执行
    • 执行 render 函数,得到最新的 React element 元素,然后继续调和子节点
  6. getSnapshotBeforeUpdate
    • getSnapshotBeforeUpdate 的执行也是在 commit 阶段
      • commit 阶段细分为 before Mutation( DOM 修改前),Mutation ( DOM 修改),Layout( DOM 修改后) 三个阶段
    • getSnapshotBeforeUpdate 发生在 before Mutation 阶段,生命周期的返回值,将作为第三个参数 __reactInternalSnapshotBeforeUpdate 传递给 componentDidUpdate
  7. componentDidUpdate
    • 执行生命周期 componentDidUpdate ,此时 DOM 已经修改完成,可以操作修改之后的 DOM,到此为止更新阶段的生命周期执行完毕

更新阶段对应生命周期执行顺序:componentWillReceiveProps(props改变) / getDerivedStateFromProps => shouldComponentUpdate => componentWillUpdate => render => getSnapshotBeforeUpdate => componentDidUpdate

# 组件销毁

  1. componentWillUnmount
    • 在一次调和更新中,如果发现元素被移除,就会打对应的 Deletion 标签 ,然后在 commit 阶段就会调用 componentWillUnmount 生命周期,接下来统一卸载组件以及 DOM 元素

# 各生命周期可以做什么

三个阶段生命周期 + 无状态组件总览图:

  • constructor
    • constructor 在类组件创建实例时调用,而且初始化的时候执行一次,所以可以在 constructor 做一些初始化的工作
    • constructor 作用
      • 初始化 state ,比如可以用来截取路由中的参数,赋值给 state
      • 对类组件的事件做一些处理,比如绑定 this , 节流,防抖等
      • 对类组件进行一些必要生命周期的劫持,渲染劫持,这个功能更适合反向继承的 HOC
  • getDerivedStateFromProps
    • 参数
      • nextProps: 父组件新传递的 props
      • prevState: 传入 getDerivedStateFromProps 待合并的 state
    • getDerivedStateFromProps 方法作为类的静态属性方法执行,内部是访问不到 this 的,它更趋向于纯函数
      • 从源码中就能够体会到 React 对该生命周期定义为取缔 componentWillMountcomponentWillReceiveProps
    • 正如它的名字一样,这个生命周期用于,在初始化和更新阶段,接受父组件的 props 数据, 可以对 props 进行格式化,过滤等操作,返回值将作为新的 state 合并到 state 中,供给视图渲染层消费
    • 只要组件更新,就会执行 getDerivedStateFromProps,不管是 props 改变,还是 setState ,或是 forceUpdate
    • getDerivedStateFromProps 作用
      • 代替 componentWillMountcomponentWillReceiveProps
      • 组件初始化或者更新时,将 props 映射到 state
      • 返回值与 state 合并完,可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件

UNSAFE_componentWillMount、UNSAFE_componentWillReceiveProps、UNSAFE_componentWillUpdate

在 React V16.3 componentWillMountcomponentWillReceivePropscomponentWillUpdate 三个生命周期加上了不安全的标识符 UNSAFE。

这三个生命周期,都是在 render 之前执行的,React 对于执行 render 函数有着像 shouldUpdate 等条件制约,但是对于执行在 render 之前生命周期没有限制,存在一定隐匿风险,如果 updateClassInstance 执行多次,React 开发者滥用这几个生命周期,可能导致生命周期内的上下文多次被执行

  • componentWillMountUNSAFE_componentWillMount
    • UNSAFE_componentWillMount 的作用还是做一些初始化操作,但是不建议在这个生命周期写
  • componentWillReceivePropsUNSAFE_componentWillReceiveProps
    • UNSAFE_componentWillReceiveProps 函数的执行是在更新组件阶段
    • 该生命周期执行驱动是因为父组件更新带来的 props 修改,但是只要父组件触发 render 函数,调用 React.createElement 方法,那么 props 就会被重新创建,生命周期 componentWillReceiveProps 就会执行了
      • 即使 props 没变,该生命周期也会执行
    • componentWillReceiveProps 作用
      • 可以用来监听父组件是否执行 render
      • 可以用来接受 props 改变,组件可以根据 props 改变,来决定是否更新 state ,因为可以访问到 this , 所以可以在异步成功回调(接口请求数据)改变 state(不过不建议这么使用)
  • componentWillUpdateUNSAFE_componentWillUpdate
  • UNSAFE_componentWillUpdate 可以意味着在更新之前,此时的 DOM 还没有更新
  • 在这里可以做一些获取 DOM 的操作
  • React 已经出了新的生命周期 getSnapshotBeforeUpdate 来代替 UNSAFE_componentWillUpdate
  • 作用
  • 获取组件更新之前的状态,比如 DOM 元素位置等
  • render
    • 所谓 render 函数,就是 jsx 的各个元素被 React.createElement 创建成 React element 对象的形式
    • 一次 render 的过程,就是创建 React.element 元素的过程
    • 可以在 render 里面做一些, createElement 创建元素 , cloneElement 克隆元素React.children 遍历 children 的操作
  • getSnapshotBeforeUpdate
    • 参数
      • prevProps:更新前的 props
      • preState:更新前的 state
    • 获取更新前的快照,可以进一步理解为 获取更新前 DOM 的状态
    • 该生命周期是在 commit 阶段的 before Mutation ( DOM 修改前),此时 DOM 还没有更新,但是在接下来的 Mutation 阶段会被替换成真实 DOM ,此时是获取 DOM 信息的最佳时期
    • getSnapshotBeforeUpdate 将返回一个值作为一个 snapShot (快照),传递给 componentDidUpdate 作为第三个参数
      • 注意:如果没有返回值会给予警告⚠️,如果没有 componentDidUpdate 也会给予警告
    • 快照 snapShot 不限于 DOM 的信息,也可以是根据 DOM 计算出来产物
    • getSnapshotBeforeUpdate 这个生命周期意义就是配合 componentDidUpdate 一起使用,计算形成一个 snapShot 传递给 componentDidUpdate,保存一次更新前的信息
  • componentDidUpdate
    • 参数
      • prevProps: 更新之前的 props
      • prevState: 更新之前的 state
      • snapshotgetSnapshotBeforeUpdate 返回的快照,可以是更新前的 DOM 信息
    • 作用
      • componentDidUpdate 生命周期执行,此时 DOM 已经更新,可以直接获取 DOM 最新状态
        • 这个函数里面如果想要使用 setState ,一定要加以限制,否则会引起无限循环
      • 接受 getSnapshotBeforeUpdate 保存的快照信息
  • componentDidMount
    • componentDidMount 生命周期执行时机和 componentDidUpdate 一样,一个是在初始化,一个是组件更新
    • 此时 DOM 已经创建完,既然 DOM 已经创建挂载,就可以做一些基于 DOM 操作,DOM 事件监听器
    • 作用
      • 可以做一些关于 DOM 操作,比如基于 DOM 的事件监听器
      • 对于初始化向服务器请求数据,渲染视图,这个生命周期也是蛮合适的
  • shouldComponentUpdate
    • 参数
      • newProps:新的 props
      • newState:新的 state
      • nextContext:新的 context
    • 作用
      • 一般用于性能优化,shouldComponentUpdate 返回值决定是否重新渲染的类组件
      • 需要重点关注的是第二个参数 newState ,如果有 getDerivedStateFromProps 生命周期 ,它的返回值将合并到 newState ,供 shouldComponentUpdate 使用
  • componentWillUnmount
    • 组件销毁阶段唯一执行的生命周期
    • 主要做一些收尾工作,比如清除一些可能造成内存泄漏的定时器,延时器,或者是一些事件监听器

# 函数组件生命周期替代方案

React hooks 也提供了 api ,用于弥补函数组件没有生命周期的缺陷。其原理主要是运用了 hooks 里面的 useEffectuseLayoutEffect

# useEffect 和 useLayoutEffect

useEffect

代码语言:javascript
复制
useEffect(() => {
  return destroy
}, dep)

  • 参数
    • 第一个参数 callback, 返回的 destorydestory 作为下一次 callback 执行之前调用,用于清除上一次 callback 产生的副作用
    • 第二个参数作为依赖项,是一个数组,可以有多个依赖项,依赖项改变,执行上一次 callback 返回的 destory ,和执行新的 effect 第一个参数 callback
  • effect 回调函数不会阻塞浏览器绘制视图
    • 对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effectcallback, React 会向 setTimeout 回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行

useLayoutEffect

useEffect 和 useLayoutEffect 的区别

  • useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。
  • useLayoutEffect callback 中代码执行会阻塞浏览器绘制
  • 修改 DOM ,改变布局就用 useLayoutEffect ,其他情况就用 useEffect

`React.useEffect` 回调函数 和 `componentDidMount` / `componentDidUpdate` 执行时机有什么区别 ?

useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect 代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdateuseLayoutEffect 更类似。

# useInsertionEffect

useInsertionEffect 是在 React v18 新添加的 hooks ,它的用法和 useEffectuseLayoutEffect 一样。

useInsertionEffect 的执行时机要比 useLayoutEffect 提前,useLayoutEffect 执行的时候 DOM 已经更新了,但是在 useInsertionEffect 的执行的时候,DOM 还没有更新。

本质上 useInsertionEffect 主要是解决 CSS-in-JS 在渲染中注入样式的性能问题。这个 hooks 主要是应用于这个场景,在其他场景下 React 不期望用这个 hooks 。

CSS-in-JS 的注入会引发哪些问题?

首先看部分 CSS-in-JS 的实现原理,拿 Styled-components 为例子,通过 styled-components,你可以使用 ES6 的标签模板字符串语法(Tagged Templates)为需要 styled 的 Component 定义一系列 CSS 属性,当该组件的JS代码被解析执行的时候,styled-components 会动态生成一个 CSS 选择器,并把对应的 CSS 样式通过 style 标签的形式插入到 head 标签里面。

动态生成的 CSS 选择器会有一小段哈希值来保证全局唯一性来避免样式发生冲突。这种模式下本质上是动态生成 style 标签。

如果在 useLayoutEffect 使用 CSS-in-JS 会造成哪些问题?

  • useLayoutEffect 执行的时机 DOM 已经更新完成,布局也已经确定了,剩下的就是交给浏览器绘制就行了
  • 如果在 useLayoutEffect 动态生成 style 标签,那么会再次影响布局,导致浏览器再次重回和重排

useInsertionEffect 的执行在 DOM 更新前,所以此时使用 CSS-in-JS 避免了浏览器出现再次重回和重排的可能,解决了性能上的问题。

# componentDidMount 替代方案

代码语言:javascript
复制
React.useEffect(() => {
  /** 请求数据,事件监听,操作 DOM */
}, []) // 第二个参数传入空数组,表示只执行一次

dep = [] ,这样当前 effect 没有任何依赖项,也就只有初始化执行一次

# componentWillUnmount 替代方案

代码语言:javascript
复制
React.useEffect(() => {
  /** 请求数据,事件监听,操作 DOM ,增加定时器,延时器 */
  return function componentWillUnmount() {
    /** 解除事件监听,清除定时器,延时器 */
  }
}, [])

componentDidMount 的前提下,useEffect 第一个函数的返回函数,可以作为 componentWillUnmount 使用。

# componentWillReceiveProps 代替方案

useEffect 代替 componentWillReceiveProps 着实有点牵强:

  • 首先因为二者的执行阶段根本不同,一个是在 render 阶段,一个是在 commit 阶段
  • 其次 useEffect 会初始化执行一次,但是 componentWillReceiveProps 只有组件更新 props 变化的时候才会执行
代码语言:javascript
复制
React.useEffect(() => {
  console.log('props changed: componentWillReceiveProps');
}, [props])

此时依赖项就是 propsprops 变化,执行此时的 useEffect 钩子。

代码语言:javascript
复制
React.useEffect(() => {
  console.log('props changed: componentWillReceiveProps');
}, [props.a])

useEffect 还可以针对 props 的某一个属性进行追踪。此时的依赖项为 props 的追踪属性。上面的例子中,props.a 变化,执行此时的 useEffect 钩子。

# componentDidUpdate 替代方案

useEffectcomponentDidUpdate 在执行时期虽然有点差别,useEffect 是异步执行, componentDidUpdate 是同步执行 ,但都是在 commit 阶段 。useEffect 会默认执行一次,而 componentDidUpdate 只有在组件更新完成后执行。

代码语言:javascript
复制
React.useEffect(() => {
  console.log('componentDidUpdate');
})

注意此时 useEffect 没有第二个参数。没有第二个参数,那么每一次执行函数组件,都会执行该 effect

# 完整的生命周期替代方案

代码语言:javascript
复制
function FunctionLifecycle(props) {
  const [num, setNum] = React.useState(0);

  React.useEffect(() => {
    /** 请求数据,事件监听,操作 DOM,增加定时器,延时器 */
    console.log('组件挂载完成 componentDidMount');
    return function componentWillUnmount() {
      /** 解除事件监听,清除定时器,延时器 */
      console.log('组件销毁 componentWillUnmount');
    }
  }, []); // 第二个参数传入空数组,表示只执行一次

  React.useEffect(() => {
    console.log('props 变化: componentWillReceiveProps');
  }, [props]);

  React.useEffect(() => {
    console.log('组件更新完成 componentDidUpdate');
  }); // 没有第二个参数,每次执行函数组件都会执行该 effect

  return (
    <div>
      <p>props: {props.number}</p>
      <p>state: {num}</p>
      <button onClick={() => setNum(state => state + 1)}>num + 1</button>
    </div>
  );
}

export default () => {
  const [number, setNumber] = React.useState(0);
  const [isRender, setIsRender] = React.useState(true);
  
  return <div>
    {isRender && <FunctionLifecycle number={number} />}
    <button onClick={() => setNumber(state => state + 1)}>number + 1</button><br/>
    <button onClick={() => setIsRender(state => !state)}>isRender</button>
  </div>;
}

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022/9/12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 生命周期
  • # 类组件生命周期
    • # 组件初始化阶段
      • # 组件更新
        • # 组件销毁
          • # 各生命周期可以做什么
          • # 函数组件生命周期替代方案
            • # useEffect 和 useLayoutEffect
              • # useInsertionEffect
                • # componentDidMount 替代方案
                  • # componentWillUnmount 替代方案
                    • # componentWillReceiveProps 代替方案
                      • # componentDidUpdate 替代方案
                        • # 完整的生命周期替代方案
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档