React从v16.3的版本开始, 对生命周期的钩子进行了渐进式的调整,分别废弃和新增了一些生命周期的钩子函数。
新旧生命周期对比
一个完整的React组件生命周期会依次调用如下钩子:
挂载
- constructor
- componentWillMount
- render
- componentDidMount
更新
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
卸载
- componentWillUnmount
挂载
- counstructor
- getDerivedStateFromProps
- render
- componentDidMount
更新
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
卸载
- componentWillUnmount
从以上生命周期的对比,我们不难看出,React从v6.3开始废弃compentWillMount、componentWillReceiveProps、componentWillUpdate三个钩子函数。
分析废弃原因
Facebook花了两年多的时间搞出了React Fiber, 因为在v15的版本,更新过程是同步的,往往一个主线程长时间被占用,会导致页面性能问题。 而 React Fiber的机制: 利用浏览器 requestIdleCallback 将可中断的任务进行分片处理,每一个小片的运行时间很短,这样唯一的线程就不会被独占。 当React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,那React就以不破楼兰终不还的气概,一鼓作气运行到底,中途绝不停歇。
表面上看,这样的设计也是挺合理的,因为更新过程不会有任何I/O操作嘛,完全是CPU计算,所以无需异步操作,的确只要一路狂奔就行了,但是,当组件树比较庞大的时候,问题就来了。 假如更新一个组件需要1毫秒,如果有200个组件要更新,那就需要200毫秒,在这200毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这200毫秒内,用户往一个input元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被React占着呢,抽不出空,最后的结果就是用户敲了按键看不到反应,等React更新过程结束之后,咔咔咔那些按键一下子出现在input元素里了。 这就是所谓的界面卡顿,很不好的用户体验。 现有的React版本,当组件树很大的时候就会出现这种问题,因为更新过程是同步地一层组件套一层组件,逐渐深入的过程,在更新完所有组件之前不停止,函数的调用栈就像下图这样,调用得很深,而且很长时间不会返回。
因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。
因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。
React Fiber把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。
维护每一个分片的数据结构,就是Fiber。
有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。
为什么叫Fiber呢?
大家应该都清楚进程(Process)和线程(Thread)的概念,在计算机科学中还有一个概念叫做Fiber,英文含义就是“纤维”,意指比Thread更细的线,也就是比线程(Thread)控制得更精密的并发处理机制。
上面说的Fiber和React Fiber不是相同的概念,但是,我相信,React团队把这个功能命名为Fiber,含义也是更加紧密的处理机制,比Thread更细。
React Fiber对现有代码的影响
在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。
因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase。
在第一阶段Reconciliation Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的;但是到了第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断。
这两个阶段大部分工作都是React Fiber做,和我们相关的也就是生命周期函数。
以render函数为界,第一阶段可能会调用下面这些生命周期函数,说是“可能会调用”是因为不同生命周期调用的函数不同。
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
下面这些生命周期函数则会在第二阶段调用。
- componentDidMount
- componentDidUpdate
- componentWillUnmount
componentWillUnmount
因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。
比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。
在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!
使用React Fiber之后,一定要检查一下第一阶段相关的这些生命周期函数,看看有没有逻辑是假设在一个更新过程中只调用一次,有的话就要改了。
我们挨个看一看这些可能被重复调用的函数。
componentWillReceiveProps,即使当前组件不更新,只要父组件更新也会引发这个函数被调用,所以多调用几次没啥,通过! shouldComponentUpdate,这函数的作用就是返回一个true或者false,不应该有任何副作用,多调用几次也无妨,通过!
render,应该是纯函数,多调用几次无妨,通过!
只剩下componentWillMount和componentWillUpdate这两个函数往往包含副作用,所以当使用React Fiber的时候一定要重点看这两个函数的实现。
详解新的生命周期
constructor() constructor() 在React组件挂载之前被调用,在为React.Component子类实现构造函数时,应在其他语句之前调用 super()
super的作用:将父类的this对象继承给子类 (MDN参考) 通常,React构造函数仅用于以下两种情况:
-
来初始化函数内部 state
-
为 事件处理函数 绑定实例
不能在 constructor()构造函数内部调用 this.setState(), 因为此时第一次 render()还未执行,也就意味DOM节点还未挂载
static getDerivedStateFromProps(nextProps, state) getDerivedStateFromProps() 在调用 render方法之前调用,在初始化和后续更新都会被调用。
返回值:返回一个对象来更新 state, 如果返回 null 则不更新任何内容。
参数: 第一个参数为即将更新的 props, 第二个参数为上一个状态的 state , 可以比较props 和 state来加一些限制条件,防止无用的state更新
注意:getDerivedStateFromProps 是一个静态函数,不能使用this, 也就是只能作一些无副作用的操作 render() render() 方法是class组件中唯一必须实现的方法,用于渲染dom, render()方法必须返回reactDOM
注意: 不要在 render 里面 setState, 否则会触发死循环导致内存崩溃
componentDidMount() componentDidMount() 在组件挂载后 (插入DOM树后) 立即调用,componentDidMount() 是发送网络请求、启用事件监听方法的好时机,并且可以在 此钩子函数里直接调用 setState()
shouldComponentUpdate(nextProps, nextState) shouldComponentUpdate() 在组件更新之前调用,可以控制组件是否进行更新, 返回true时组件更新, 返回false则不更新 getSnapshotBeforeUpdate(prevProps, prevState) getSnapshotBeforeUpdate() 在最近一次的渲染输出被提交之前调用。也就是说,在 render 之后,即将对组件进行挂载时调用。
它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给 componentDidUpdate()。如不需要传递任何值,那么请返回 null componentDidUpdate(prevProps, prevState, snapshot) componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行 componentWillUnmount()
此生命周期是取消网络请求、移除监听事件、清理 DOM 元素、清理定时器等操作的好时机
从五种组件状态改变的时机来探究生命周期的执行顺序
一、父子组件初始化
父子组件第一次进行渲染加载时:
-
Parent 组件: constructor()
-
Parent 组件: getDerivedStateFromProps()
-
Parent 组件: render()
-
Child 组件: constructor()
-
Child 组件: getDerivedStateFromProps()
-
Child 组件: render()
-
Child 组件: componentDidMount()
-
Parent 组件: componentDidMount() 二、子组件修改自身状态 state
-
Child 组件: getDerivedStateFromProps()
-
Child 组件: shouldComponentUpdate()
-
Child 组件: render()
-
Child 组件: getSnapshotBeforeUpdate()
-
Child 组件: componentDidUpdate() 三、修改父组件中传入子组件的 props
-
Parent 组件: getDerivedStateFromProps()
-
Parent 组件: shouldComponentUpdate()
-
Parent 组件: render()
-
Child 组件: getDerivedStateFromProps()
-
Child 组件: shouldComponentUpdate()
-
Child 组件: render()
-
Child 组件: getSnapshotBeforeUpdate()
-
Parent 组件: getSnapshotBeforeUpdate()
-
Child 组件: componentDidUpdate()
-
Parent 组件: componentDidUpdate()
四、卸载子组件
-
Parent 组件: getDerivedStateFromProps()
-
Parent 组件: shouldComponentUpdate()
-
Parent 组件: render()
-
Parent 组件: getSnapshotBeforeUpdate()
-
Child 组件: componentWillUnmount()
-
Parent 组件: componentDidUpdate()
五、重新挂载子组件
-
Parent 组件: getDerivedStateFromProps()
-
Parent 组件: shouldComponentUpdate()
-
Parent 组件: render()
-
Child 组件: constructor()
-
Child 组件: getDerivedStateFromProps()
-
Child 组件: render()
-
Parent 组件: getSnapshotBeforeUpdate()
-
Child 组件: componentDidMount()
-
Parent 组件: componentDidUpdate()
父子组件生命周期执行顺序总结:
- 当子组件自身状态改变时,不会对父组件产生副作用的情况下,父组件不会进行更新,即不会触发父组件的生命周期
- 当父组件中状态发生变化(包括子组件的挂载以及卸载)时,会触发自身对应的生命周期以及子组件的更新
- render 以及 render 之前的生命周期,则 父组件先执行
- render 以及 render之后的声明周期,则子组件先执行,并且是与父组件交替执行
- 当子组件进行卸载时,只会执行自身的 componentWillUnmount 生命周期,不会再触发别的生命周期
getDerivedStateFromProps
总有开发者问我,为什么不在componentWillMount里写AJAX获取数据的功能,他们的观点是,componentWillMount在render之前执行,早一点执行早得到结果。要知道,在componentWillMount里发起AJAX,不管多快得到结果也赶不上首次render,而且componentWillMount在服务器端渲染也会被调用到(当然,也许这是预期的结果),这样的IO操作放在componentDidMount里更合适。在Fiber启用async render之后,更没有理由在componentWillMount里做AJAX,因为componentWillMount可能会被调用多次,谁也不会希望无谓地多次调用AJAX吧。 道理说了都明白,但是历史经验告诉我们,不管多么地苦口婆心教导开发者不要做什么不要做什么,都不如直接让他们干脆没办法做。 随着getDerivedStateFromProps的推出,同时deprecate了一组生命周期API,包括:
-
componentWillReceiveProps
-
componentWillMount
-
componentWillUpdate
按照官方说法,以前需要利用被deprecate的所有生命周期函数才能实现的功能,都可以通过getDerivedStateFromProps的帮助来实现。
这个getDerivedStateFromProps是一个静态函数,所以函数体内不能访问this,简单说,就是应该一个纯函数,纯函数是一个好东西啊,输出完全由输入决定。
static getDerivedStateFromProps(nextProps, prevState) {
//根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
}
每当父组件引发当前组件的渲染过程时,getDerivedStateFromProps会被调用,这样我们有一个机会可以根据新的props和之前的state来调整新的state,如果放在三个被deprecate生命周期函数中实现比较纯,没有副作用的话,基本上搬到getDerivedStateFromProps里就行了;如果不幸做了类似AJAX之类的操作,首先要反省为什么自己当初这么做,然后搬到componentDidMount或者componentDidUpdate里面去。