react生命周期详解以及函数组件的替代方案

331 阅读7分钟

生命周期的简单说法

react在render阶段会深度遍历react fiber 树,目的就是发现不同(diff),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行render函数,在一次render过程中结束后,就回到commit阶段,commit阶段会创建修改真实的DOM节点。

生命周期执行过程

1. 初始化阶段

  • constructor 执行

在mount阶段,首先执行的是constructClassInstance函数,用来实例化React组件,在组件章节已经介绍了这个函数,组件中constructor就是在这里执行的。

在组件实例化之后,它会调用mountClassInstance组件初始化

  • getDerivedStateFormProps执行

在初始化阶段,getDerivedStateFromProps是第二个执行的生命周期,值得注意的是它是从ctor类上直接绑定的静态方法,传入props,State。返回值将之前的state合并,作为新的state,传递给组件实例使用.

  • componentWillMount 执行
  • render 函数执行
  • componentDidMount执行

从这一步之前react其实一直在render阶段进行执行。一旦react,调和完所有的fiber节点,就会到commit阶段,在组件初始化commit阶段,会调用componentDidMount生命周期。

react 开始的执行顺序为:constructor -> getDerivedStateFromProps / componentWillMount -> rednder -> componentDidMount

2. 更新阶段

  • 执行生命周期componentWillReceiveProps

首先判断getDerivedStateFromProps 生命周期是否存在,如果不存在就执行componentWillReceiveProps生命周期,传入该生命周期有两个参数,分别为newProps和nextContext。

  • 执行 getDerivedStateFromProps

上述下来执行它,返回的值用于合并state,生成新的state。

  • 执行shouldComponentUpdate

下来执行它,传入新的props,新的state,和新的context,返回值决定是否继续执行render函数,tiao'he 子节点。

  • 执行componentWillUpdate

updateClassInstance方法到此结束。

  • 执行render

执行render,得到最新的react element 元素,然后继续调和子节点。

  • 执行getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 的执行也是在 commit 阶段,commit 阶段细分为 before Mutation( DOM 修改前),Mutation ( DOM 修改),Layout( DOM 修改后) 三个阶段,getSnapshotBeforeUpdate 发生在before Mutation 阶段,生命周期的返回值,将作为第三个参数 __reactInternalSnapshotBeforeUpdate 传递给 componentDidUpdate 。

  • 执行componentDidUpdate

这里dom已经修改完成,可以操作修改之后的dom。

各个生命周期的能力

  • constructor

constructor 在类组件创建实例时调用,而且初始化的时候执行一次,所以可以在 constructor 做一些初始化的工作。

  • componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot){
    const style = getComputedStyle(this.node)
    const newPosition = { /* 获取元素最新位置信息 */
        cx:style.cx,
        cy:style.cy
    }
}

三个参数:

  • prevProps 更新之前的 props ;
  • prevState 更新之前的 state ;
  • snapshot 为 getSnapshotBeforeUpdate 返回的快照,可以是更新前的 DOM 信息。

作用

  • componentDidUpdate 生命周期执行,此时 DOM 已经更新,可以直接获取 DOM 最新状态。这个函数里面如果想要使用 setState ,一定要加以限制,否则会引起无限循环。
  • 接受 getSnapshotBeforeUpdate 保存的快照信息。
  • componentDidMount

componentDidMount 生命周期执行时机和 componentDidUpdate 一样,一个是在初始化,一个是组件更新。此时 DOM 已经创建完,既然 DOM 已经创建挂载,就可以做一些基于 DOM 操作,DOM 事件监听器。

async componentDidMount(){
    this.node.addEventListener('click',()=>{
        /* 事件监听 */
    }) 
    const data = await this.getData() /* 数据请求 */
}

3. 销毁阶段

执行生命周期 componentWillUnmount

在一次调和更新中,如果发现元素被移除,就会调用 componentWillUnmount 生命周期,接下来统一卸载组件以及 DOM 元素。

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

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

useEffect 和 useLayoutEffect

useEffect

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 替代方案

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

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

componentWillUnmount 替代方案

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

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

componentWillReceiveProps 代替方案

useEffect 代替 componentWillReceiveProps 着实有点牵强:

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

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

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

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

componentDidUpdate 替代方案

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

React.useEffect(() => {
  console.log('componentDidUpdate');
})

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

完整的生命周期替代方案

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>;
}