1、新旧生命周期
渲染工作流:组件数据改变到组件实际更新发生的过程。
1.1 React15生命周期简介(不重点关注)
constructor()
componentWillReceiveProps()
shouldComponentUpdate()
componentWillMount()
componentWillUpdate()
componentDidUpdate()
componentDidMount()
render()
componentWillUnmount()
1.2 React16生命周期解读
1.2.1 constructor
constructor 在类组件创建实例时调用,而且初始化的时候执行一次,可以在 constructor 做一些初始化的工作。
constructor(props){
super(props) // 执行 super ,别忘了传递props,才能在接下来的上下文中,获取到props。
this.state={ //① 可以用来初始化state,比如可以用来获取路由中的
name:'alien'
}
this.handleClick = this.handleClick.bind(this) /* ② 绑定 this */
this.handleInputChange = debounce(this.handleInputChange , 500) /* ③ 绑定防抖函数,防抖 500 毫秒 */
const _render = this.render
this.render = function(){
return _render.bind(this) /* ④ 劫持修改类组件上的一些生命周期 */
}
}
constructor 作用:
- 初始化 state ,比如可以用来截取路由中的参数,赋值给 state 。
- 对类组件的事件做一些处理,比如绑定 this , 节流,防抖等。
- 对类组件进行一些必要生命周期的劫持,渲染劫持。
1.2.2 getDerivedStateFromProps
// nextProps 父组件新传递的 props ;
// prevState 组件在此次更新前的 state 。
getDerivedStateFromProps(nextProps,prevState)
只要组件更新,就会执行 getDerivedStateFromProps,不管是 props 改变,还是 setState ,或是 forceUpdate 。
getDerivedStateFromProps 作用:
- 代替 componentWillMount 和 componentWillReceiveProps
- 组件初始化或者更新时,将 props 映射到 state。
- 返回值与 state 合并完,可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件。(请不要把 getDerivedStateFromProps 和 shouldComponentUpdate 强行关联到一起,两者没有必然联系)
getDerivedStateFromProps 直接被定义为 static 方法 —— static 方法内部拿不到组件实例的 this,这就导致你无法在 getDerivedStateFromProps 里面做任何类似于 this.fetch()、不合理的 this.setState(会导致死循环的那种)这类可能会产生副作用的操作。
1.2.3 render
可以在render里面做一些,createElement创建元素 , cloneElement 克隆元素 ,React.children 遍历 children 的操作。
1.2.4 componentDidMount
componentDidMount(){}
作用:
- 可以做一些关于 DOM 操作,比如基于 DOM 的事件监听器。
- 对于初始化向服务器请求数据,渲染视图,这个生命周期也是蛮合适的。
1.2.5 shouldComponentUpdate
shouldComponentUpdate(newProps,newState,nextContext){}
shouldComponentUpdate 三个参数,第一个参数新的 props ,第二个参数新的 state ,第三个参数新的 context 。
作用:
- 这个生命周期,一般用于性能优化,shouldComponentUpdate 返回值决定是否重新渲染的类组件。需要重点关注的是第二个参数 newState ,如果有 getDerivedStateFromProps 生命周期 ,它的返回值将合并到 newState ,供 shouldComponentUpdate 使用。
1.2.6 getSnapshotBeforeUpdate
// prevProps更新前的props ;
// preState更新前的state;
getSnapshotBeforeUpdate(prevProps,preState){
return ...
}
getSnapshotBeforeUpdate 将返回一个值作为一个snapShot(快照),传递给 componentDidUpdate作为第三个参数。
作用:
- getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息。
getSnapshotBeforeUpdate(prevProps, prevState) {
return ...
}
componentDidUpdate(preProps, preState, valueFromSnapshot) {
console.log("componentDidUpdate方法执行");
console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot);
}
1.2.7 componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot){}
三个参数:
- prevProps 更新之前的 props ;
- prevState 更新之前的 state ;
- snapshot 为 getSnapshotBeforeUpdate 返回的快照,可以是更新前的 DOM 信息;
作用:
- componentDidUpdate 生命周期执行,此时 DOM 已经更新,可以直接获取 DOM 最新状态。这个函数里面如果想要使用 setState ,一定要加以限制,否则会引起无限循环。
- 接受 getSnapshotBeforeUpdate 保存的快照信息。
1.2.8 componentWillUnmount
componentWillUnmount(){
clearTimeout(this.timer) /* 清除延时器 */
this.node.removeEventListener('click',this.handerClick) /* 卸载事件监听器 */
}
componentWillUnmount 是组件销毁阶段唯一执行的生命周期,主要做一些收尾工作,比如清除一些可能造成内存泄漏的定时器,延时器,或者是一些事件监听器。
作用:
- 清除延时器,定时器。
- 一些基于 DOM 的操作,比如事件监听器。
1.3 新增生命周期特点解读
getDerivedStateFromProps 不是 componentWillMount 的替代品,它仅有一个用途:使用 props 来派生/更新 state;
getDerivedStateFromProps 在更新和挂载两个阶段都会使用;
在使用getDerivedStateFromProps,需要把握三个重点。
- getDerivedStateFromProps 是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this 的。
- 该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state。
- getDerivedStateFromProps 需要一个对象格式的返回值。如果你没有指定这个返回值,那么大概率会被 React 警告。
getDerivedStateFromProps 的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的 state。
getDerivedStateFromProps 方法对 state 的更新动作并非“覆盖”式的更新,而是针对某个属性的定向更新。
2、Fiber 架构
Fiber 会使原本同步的渲染过程变成异步的。
2.1 单线程的 JavaScript 与多线程的浏览器
JavaScript 是单线程的,浏览器是多线程的。
对于多线程的浏览器来说,它除了要处理 JavaScript 线程以外,还需要处理包括事件系统、定时器/延时器、网络请求等各种各样的任务线程,这其中,自然也包括负责处理 DOM 的UI 渲染线程。而 JavaScript 线程是可以操作 DOM 的。
JavaScript 线程和渲染线程必须是互斥的,必须串行,当其中一个线程执行时,另一个线程只能挂起等待。
如果JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,带给用户的体验就是所谓的卡顿。
2.2 产生卡顿的原因
协调所带来的一个问题是JavaScript 对主线程的超时占用问题。协调是一个同步的递归过程,不可以被打断。当处理结构相对复杂、体量相对庞大的虚拟 DOM 树时,协调需要的调和时间会很长,这就意味着 JavaScript 线程将长时间地霸占主线程,进而导致渲染卡顿/卡死、交互长时间无响应等问题。
2.3 Fiber 是如何解决问题的
Fiber 架构的应用目的是实现“增量渲染”。通俗来说就是把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里面。实现增量渲染的目的,是为了实现任务的可中断、可恢复,并给不同的任务赋予不同的优先级,最终达成更加顺滑的用户体验。
2.4 Fiber架构对生命周期的影响
- render 阶段:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。
- pre-commit 阶段:可以读取 DOM。
- commit 阶段:可以使用 DOM,运行副作用,安排更新。
总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。
由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,用户是可见的。
3、为什么要废除旧的生命周期
在 Fiber 机制下,render 阶段是允许暂停、终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。
都处于 render 阶段,以下方法都可能重复被执行。
- componentWillMount;
- componentWillUpdate;
- componentWillReceiveProps。
componentWill开头的生命周期里,习惯做的错误操作
- setState();
- fetch 发起异步请求;
- 操作真实 DOM。
如何避免:
(1)完全可以转移到其他生命周期(尤其是 componentDidxxx)里去做。
比如在 componentWillMount 里发起异步请求。componentWillMount 结束后,render 会迅速地被触发,所以说首次渲染依然会在数据返回之前执行。
(2)在 Fiber 带来的异步渲染机制下,可能会导致非常严重的 Bug。
假如你在 componentWillxxx 里发起了一个付款请求。由于 render 阶段里的生命周期都可以重复执行,在 componentWillxxx 被打断 + 重启多次后,就会发出多个付款请求。
getDerivedStateFromProps 为何会在设计层面直接被约束为一个触碰不到 this 的静态方法,其背后的原因也就更加充分了——避免开发者触碰 this,就是在避免各种危险的骚操作。
(3)避免滥用
比如在 componentWillReceiveProps 和 componentWillUpdate 里滥用 setState 导致重复渲染死循环。
4、结论
React 16 改造生命周期的主要动机是为了配合 Fiber 架构带来的异步渲染机制。
针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践。这一系列的工作做下来,首先是确保了 Fiber 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。
5、问答
1、问:当 props 不变的前提下, PureComponent 组件能否阻止 componentWillReceiveProps 执行?
答案是否定的,componentWillReceiveProps 生命周期的执行,和纯组件没有关系,纯组件是在 componentWillReceiveProps 执行之后浅比较 props 是否发生变化。所以 PureComponent 下不会阻止该生命周期的执行。
2、问:React.useEffect 回调函数 和 componentDidMount / componentDidUpdate 执行时机有什么区别 ?
答:useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。