React生命周期中有哪些坑?如何避免?

React生命周期中有哪些坑?如何避免?_第1张图片

在讨论React 的生命周期的时候,一定是在讨论类组件,因为函数组件并没有生命周期的概念,它本身就是一个函数,只会从头执行到尾巴

其实生命周期只是一个抽象的概念,大部分人看到生命周期想到的往往都componentDidMount,componentWilMount等等函数,然而这些其实并不是它的生命周期,只是在生命周期中按顺序执行的函数而已,挂载 --> 更新 --> 卸载 这一React的完整流程才叫生命周期

挂载阶段 -- 指的是组件从初始化到完成加载的过程

constructor

constructor 是类通用的构造函数,常用于初始化,所以在过去,constructor 常用于初始化state和绑定函数,例如:

import React from 'react';

class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
    }
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
     // do some thing
  }
  render() {
     return null
  }
}

那为什么会强调过去呢,因为当类属性开始流行之后,React 社区的写法发生了变化,即去除了 constructor。

import React from 'react'
class Counter extends React.Component {
   state = {
      count: 0,
   }

   // 类属性第三阶段提案
   handleClick = () => {
     // do some thing
   }
  render() {
     return null
  }
}

社区去除 constructor 的原因很明确:

  • constructor 中并不推荐去处理初始化以外的逻辑
  • constructor 本身并不是属于React的生命周期,它只是Class的一个初始化函数
  • 通过移除constructor, 代码会变的更加的简洁

getDerivedStateFromProps

这个函数的作用是当 props 发生变化时来更新state,那么它的触发时机是什么时候呢?

  • 当 props 被传入的时候
  • 当 state 发生变化时
  • 当 forceUpdate 被调用时

最常见的一个错误就是误认为只有当props发生变化时,getDerivedStateFromProps 才会被调用,而实际上只要父组件重新渲染时,getDerivedStateFromProps 就会被调用,所以是外部参数,

也就是 props 传入时就会发生变化。下面是官方文档中的列子:

class Example extends React.Component {
   state = {
      isScrollingDown: false,
      lastRow: null
   };

   static getDerivedStateFromProps(props, state) {
     if (props.currentRow !== state.lastRow){
        return {
           isScrollingDown: props.currentRow > state.lastRow,
           lastRow: props.currentRow,
        }
     }
    // 返回 null 表示无需要更新state
    return null  
   }
}

 依据官方的说法,它的使用场景是很有限的。

两种反模式的使用方式:

  1. 直接复制到 props 到 state
  2. 在 props 变化后修改 state

这两种写法除了增加代码的维护成本外,没有带来任何的好处。

UNSAFE_componentWillMount

也就是componentWillMount,在组件即将被挂载之前执行某些操作,目前已被弃用,因为在React的新的异步渲染机制下,该方法可能会被多次调用。

写过服务端渲染的同学应该会遇到过, componentWillMount 跟服务器端同构渲染的时候,如果在该函数中发起网络请求话,会发现请求在服务器和客户端各执行了一次,所以React 更推荐在componentDidMount 中去做网络请求等操作

render

render 函数返回的是 JSX 的数据结构,用于描述具体的渲染内容,但是 render函数并不会去真正的渲染组件,真正的渲染是依靠 React 操作 JSX 描述结构来完成,而且 render 函数应该是一个纯函数,不应该在里面产生副作用,比如调用 setState 函数(render 函数在每次渲染的时候都会被调用,而 setState 又会触发渲染,所以就会造成死循环)

componentDidMount

componentDidMount 用于在组件挂载完成时去做某些操作,比如发起网络请求等,componentDidMount 是在render之后被调用

至此 挂载阶段 基本算是完成

更新阶段

更新阶段是指:当外部 props 被传入,又或者当 state 发生改变时的阶段。

UNSAFE_componentWillReceiveProps

该函数已标记弃用,因为其功能可被 getDerviedStateFromProps 所替代

另外,当 getDerviedStateFromProps 存在时 UNSAFE_componentWillReceiveProps 不会被调用

getDerviedStateFromProps

同挂载阶段的表现一致。

shouldComponentUpdate

该方法通过返回true或者fasle来确定是否重新触发新的渲染,这也是性能优化的必争之地,通过添加判断条件来控制组件是否需要重新渲染

当前 React 的官方也给出了一个通用的优化方案,那就是PureComponent,PureComponent 的核心原理就是默认实现了一个 shouldComponentUpdate 函数,在这个函数中对 props 和 state 进行浅比较,用来判断是否重新触发更新。

shouldComponentUpdate(nextProps, nextState) {
  // 浅比较仅比较值与引用,并不会对 Object 中的每一项值进行比较
  if (shadowEqual(nextProps, this.props) || shadowEqual(nextState, this.state) ) {
    return true
  }
  return false
}

UNSAFE_componentWillUpdate

该方法也被标记弃用,因为在后续的 React 的异步渲染设计中,可能会出现组件暂停更新渲染的情况

render

同挂载阶段的表现一致

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 方法是配合 React 新的异步渲染的机制,在DOM更新发生前被调用,其返回值将作为 componentDidUpate 的第三个参数

官方案例:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }
 
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }
 
  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }
 
  render() {
    return (
      
{/* ...contents... */}
); } }

componentDidUpdate

如上述案例,getSnapshotBeforeUpdate 的返回值会作为 componentDidUpdate 的第三个参数适用。

copinentDidUpdate 中可以设置 setState,会触发渲染,慎用,避免死循环

至此 更新阶段 基本算是完成

卸载阶段

React 的卸载阶段就容易多啦,只有一个函数

componentWillUnmount

该函数的作用就是用来清理工作,如果项目中有用到定时器啥的,一定记得要在这清除掉,否则就会一直执行~

职责梳理

在梳理了生命周期后,需要注意两个问题

  • 什么情况下会触发重新渲染
  • 渲染过程中的报错该如何处理

带着上述两个问题,下面咱们来一一分析3种重新渲染和错误处理的情况

函数组件

函数组件在任何情况下都会重新渲染,它并没有生命周期,但是官方提供了一种优化的方式,那就是 React.memo 函数

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

React.memo 并不会阻断渲染,而是跳过渲染组件的操作并直接复用最近一次渲染的结果,这与 shouldComponentUpdate 是完全不同的

React.Component

如果不实现 shouldComponentUpdate 函数,那么下面这两种情况回引发重新渲染

  1. 当 state 发生变化时
  2. 当父组件的 Props 传入时,无论 props 有没有发生变化, 只要传入就会引发重新渲染

React.PureComponent

PureComponent 默认实现了 shouldComponentUpdate 函数。所以仅在 props 与 state 进行浅比较后,确认有变更时才会触发重新渲染。

错误边界

错误边界其实就是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染备用组件,如下官方案例

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    // 同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 可以自定义降级后的 UI 并渲染
      return 

Something went wrong.

; } return this.props.children; } }

但是渲染时的报错,只能通过componentDidCatch 捕获,这是在做线上页面报错监控时,极其容易忽略的点。

综上所述,咱们就可以总结这个问题了,如下:

避免生命周期中的坑需要做好两件事:

1、不在恰当的时候调用了不该调用的代码;

2、在需要调用时,不要忘了调用。

那么下面7种情况最容易造成生命周期的坑:

  • getDerivedStateFromProps 容易编写反模式代码,使受控组件和非受控组件区分模糊
  • componentWillMount 在 React 中已被标记弃用,不推荐使用,主要的原因是因为新的异步架构会导致它被多次调用,所以网络请求以及事件绑定应该放到 componentDidMount 中
  • componentWillReceiveProps 同样也被标记弃用,被 getDerivedStateFromProps 所取代,主要原因是性能问题。
  • shouldComponentUpdate 通过返回 true 或者 false 来确定是否需要触发新的渲染。主要用于性能优化。
  • componentWillUpdate 同样是由于新的异步渲染机制,而被标记废弃,不推荐使用,原先的逻辑可结合 getSnapshotBeforeUpdate 与 componentDidUpdate 改造使用。
  • 如果在 componentWillUnmount 函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 bug。
  • 如果没有添加错误边界处理,当渲染发生异常时,用户将会看到一个无法操作的白屏,所以一定要添加。

追问:React 的请求应该放到哪里?为什么?

  • 对于异步请求,应该放到componentDidMount 中去操作,从生命周期函数执行顺序来看,除了componentDidMount 之外还有下面两种选择:
    • constructor 中可以放,但是不推荐,因为 constructor 主要用于初始化 state 和函数绑定,并不承载业务逻辑,而且随着类属性的流行,constructor 很少用
    • componentWillMount 已被标记废弃,因为在新的异步渲染下该方法会触发多次渲染,容易引发bug,不利于 React 的后期维护和更新
  • 所以React 的请求放在 componentDidMount 里是最好的选择

你可能感兴趣的:(React,react.js,前端)