React生命周期

497 阅读7分钟

1.共有哪些生命周期函数

react的生命周期函数共有:constructor、getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidMount、****component­Did­Update、****component­Will­Unmount八个生命周期函数。

其中红色为常用

2.react生命周期函数的执行顺序

通常react组件存在三种行为–“挂载”、“更新”、“卸载”,因此由这三种行为对应的生命周期函数由下图(生命周期图谱)所示:

3.为何React v 16.0取消了下面这几种生命周期函数

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

React的这个生命周期函数更新是基于React Fiber的,由于Fiber重写了React的dom更新策略,将一个更新过程分为了两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase。

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。

在第一阶段Reconciliation Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的;但是到了第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断。

这里可以理解为以render函数为界,以上4个生命周期函数均为在render之前执行,而遗留下来的componentDidMount、component­Did­Update、component­Will­Unmount则是在render函数执行之后再执行。

img

因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!

---

总的来说,及被舍弃的生命周期函数可能存在副作用,这里我的理解是这些函数可能会对state进行更改即调用setState方法,而该方法的执行是异步的,由于任务优先级的关系,该生命周期可能会多次重复执行setState方法多次(开发人员只希望执行一次),因此被舍弃,只保留在render之后的生命周期函数,保证在每个组件渲染时只执行一次生命周期函数。

而这里shouldComponentUpdate被遗留了下来,是因为该函数一般不会存在副作用,并且需要一个这样的函数入口来控制

---

4.生命周期函数的使用场景

constructor(props)

构造函数一般就做两件事情:

super(props)、设置初始state

super(props)主要是为了在constructor中可以调用this

这里设置初始state不需要用setState,直接用this.state进行设置,并且这里的初始值要避免加入props

目前若在初始化state不需要props,可直接在class中对state进行赋值

componentDidMount()

该函数会在组件挂载后(插入 DOM 树中)立即调用。

该函数一般用于依赖于 DOM 节点的初始化、通过网络请求数据、添加订阅

componentDidUpdate(prevProps, prevState, snapshot)

该函数会在更新后会被立即调用。首次渲染不会执行此方法。

该函数一般用于比较更新前后的props后setState或进行网络请求,但不论是做什么,都需要将其放在一个限定条件里,否则会导致死循环。

场景主要为在挂载后props的改变引发的组件更新,通过prevProps, prevState取得之前组件props和state,然后通过snapshot拿到真实dom的状态

componentWillUnmount()

该函数在组件卸载及销毁之前直接调用。

因此该函数主要应用于给组件“擦屁股”,即清定时器、清订阅、清请求等行为。

该函数中调用的setState方法不会导致重新渲染。

shouldComponentUpdate(nextProps, nextState)

该函数会在渲染执行之前被调用,返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

返回一个bool类型判断该组件的输出是否受当前 state 或 props 更改的影响

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

该函数返回 false 并不会阻止子组件在 state 更改时重新渲染。一般不用这个函数

static getDerivedStateFromProps(nextProps, prevState)

该方法会在调用 render 方法之前调用,仔细的说应该是来自父组件的props改变后引发的rerender前计算出新的state。其在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

该函数是一个静态函数,因此函数内部不能访问this,即该函数为纯函数。因此该函数内的行为应该是没有副作用的。

这个函数给我的感觉就是一个加了计算的setState

静态相当于在原型对象中的属性

这里同一类的每个组件 getDerivedStateFromProps 都相同

getSnapshotBeforeUpdate(prevProps, prevState)

该函数在render之后,渲染输出(提交到DOM节点)之前调用。

此生命周期的任何返回值将作为参数传递给 componentDidUpdate()

snapshot即快照的意思,即在实际dom更新之前,给当前状态一个快照,并将其传给 componentDidUpdate()

使用场景我理解为获取dom节点信息做出的调整时需要通过该函数把dom信息传递给 componentDidUpdate(),并在 componentDidUpdate()中更新state

5.其他api

cloneElement()

React.cloneElement(
  element,
  [props],
  [...children]
)

element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 keyref 将被保留。 该方法将组件的结构与数据解耦,使子组件的 props 即可以通过组件内部定义,又可以通过在结构组件中显示传递。并且同时将 keyref 保留了下来。

class App extends React.Component {

  render() {
    return (
      <>
        <MyContainer>
          <MySub subInfo={1} />
          <MySub subInfo={2} />
        </MyContainer>
      </>
    )
  }
}

class MyContainer extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 1
    }
  }

  handleClick = () => {
    this.setState({
      count: this.state.count+1
    })
  }

  render() {
    const childrenWithProps = React.Children.map(this.props.children, child => React.cloneElement(child, 
      {
        parentState: this.state.count,
        handleClick: this.handleClick
      }
    ));
    return (
      <div>
        <span>父容器:</span>
        { childrenWithProps }
      </div>
    )
  }
}
class MySub extends Component {
  constructor(props) {
      super(props)
      this.state = {
          flag: false
      }
  }

  render() {
    return (
      <div>
        子元素:{this.props.subInfo}
        <br/>
        父组件属性count值: { this.props.parentState }
        <br/>
        <span onClick={ () => this.props.handleClick() }  
        >click me</span>
      </div>
    )
  }
}

React.forwardRef

React.forwardRef 会创建一个 React 组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

  1. 转发 refs 到 DOM 组件
  2. 在高阶组件中转发 refs

React.forwardRef 接受渲染函数作为参数。React 将使用 propsref 作为参数来调用此函数。此函数应返回 React 节点。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

该方法使 dom 通过 ref 转发(一般为 props 的方式传递)的操作拿到真实的 dom 节点,从而实现对 dom 的事件监听(如hover,mousexxx等),否则拿到的上层的 dom 。

React.lazy React.lazy() 允许你定义一个动态加载的组件。

// 这个组件是动态加载的
const SomeComponent = React.lazy(() => import('./SomeComponent'));

即用 promise 的方式动态引入组件,该方法必须与 React.Suspense 配合使用。

React.Suspense

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}