预习:生命周期的使用(只针对新生命周期)
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 , 节流,防抖等。
- 对类组件进行一些必要生命周期的劫持,渲染劫持。
2、getDerivedStateFromProps
// nextProps 父组件新传递的 props ;
// prevState 组件在此次更新前的 state 。
getDerivedStateFromProps(nextProps,prevState)
只要组件更新,就会执行 getDerivedStateFromProps,不管是 props 改变,还是 setState ,或是 forceUpdate 。
getDerivedStateFromProps 作用:
- 代替 componentWillMount 和 componentWillReceiveProps
- 组件初始化或者更新时,将 props 映射到 state。
- 返回值与 state 合并完,可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件。(请不要把 getDerivedStateFromProps 和 shouldComponentUpdate 强行关联到一起,两者没有必然联系)
3、render
可以在render里面做一些,createElement创建元素 , cloneElement 克隆元素 ,React.children 遍历 children 的操作。
4、componentDidMount
componentDidMount(){}
作用:
- 可以做一些关于 DOM 操作,比如基于 DOM 的事件监听器。
- 对于初始化向服务器请求数据,渲染视图,这个生命周期也是蛮合适的。
5、shouldComponentUpdate
shouldComponentUpdate(newProps,newState,nextContext){}
shouldComponentUpdate 三个参数,第一个参数新的 props ,第二个参数新的 state ,第三个参数新的 context 。
作用:
- 这个生命周期,一般用于性能优化,shouldComponentUpdate 返回值决定是否重新渲染的类组件。需要重点关注的是第二个参数 newState ,如果有 getDerivedStateFromProps 生命周期 ,它的返回值将合并到 newState ,供 shouldComponentUpdate 使用。
6、getSnapshotBeforeUpdate
// prevProps更新前的props ;
// preState更新前的state;
getSnapshotBeforeUpdate(prevProps,preState){
return ...
}
getSnapshotBeforeUpdate 将返回一个值作为一个snapShot(快照),传递给 componentDidUpdate作为第三个参数。
作用:
- getSnapshotBeforeUpdate 这个生命周期意义就是配合componentDidUpdate 一起使用,计算形成一个 snapShot 传递给 componentDidUpdate 。保存一次更新前的信息。
7、componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot){}
三个参数:
- prevProps 更新之前的 props ;
- prevState 更新之前的 state ;
- snapshot 为 getSnapshotBeforeUpdate 返回的快照,可以是更新前的 DOM 信息;
作用
- componentDidUpdate 生命周期执行,此时 DOM 已经更新,可以直接获取 DOM 最新状态。这个函数里面如果想要使用 setState ,一定要加以限制,否则会引起无限循环。
- 接受 getSnapshotBeforeUpdate 保存的快照信息。
8、componentWillUnmount
componentWillUnmount(){
clearTimeout(this.timer) /* 清除延时器 */
this.node.removeEventListener('click',this.handerClick) /* 卸载事件监听器 */
}
componentWillUnmount 是组件销毁阶段唯一执行的生命周期,主要做一些收尾工作,比如清除一些可能造成内存泄漏的定时器,延时器,或者是一些事件监听器。
作用
- 清除延时器,定时器。
- 一些基于 DOM 的操作,比如事件监听器。
一、生命周期背后的设计思想
组件和虚拟 DOM
1. 虚拟DOM
2. 组件化
在React中,每个组件既是“封闭”的,也是“开放”的。
所谓“封闭”,主要是针对“渲染工作流”(指从组件数据改变到组件实际更新发生的过程)来说的。在组件自身的渲染工作流中,每个组件都只处理它内部的渲染逻辑。在没有数据流交互的情况下,组件与组件之间可以做到“各自为政”。
而所谓“开放”,则是针对组件间通信来说的。React 允许开发者基于“单向数据流”的原则完成组件间的通信。而组件之间的通信又将改变通信双方/某一方内部的数据,进而对渲染结果构成影响。所以说在数据这个“红娘”的牵线搭桥之下,组件之间又是彼此开放的,是可以相互影响的。
这一“开放”与“封闭”兼具的特性,使得 React 组件既专注又灵活,具备高度的可重用性和可维护性。
二、新旧生命周期方法
渲染工作流:组件数据改变到组件实际更新发生的过程。
1. React15生命周期
constructor()
componentWillReceiveProps()
shouldComponentUpdate()
componentWillMount()
componentWillUpdate()
componentDidUpdate()
componentDidMount()
render()
componentWillUnmount()
2. React16生命周期
3. 新旧生命周期对比
getDerivedStateFromProps 不是 componentWillMount 的替代品,它有且仅有一个用途:使用 props 来派生/更新 state。
getDerivedStateFromProps 在更新和挂载两个阶段都会使用
在使用getDerivedStateFromProps,需要把握三个重点。
- getDerivedStateFromProps 是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this 的。
- 该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state。
- getDerivedStateFromProps 需要一个对象格式的返回值。如果你没有指定这个返回值,那么大概率会被 React 警告。
getDerivedStateFromProps 的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的 state。
getDerivedStateFromProps 方法对 state 的更新动作并非“覆盖”式的更新,而是针对某个属性的定向更新。
4. 为什么要用 getDerivedStateFromProps 代替 componentWillReceiveProps?
React 16 在强制推行“只用 getDerivedStateFromProps 来完成 props 到 state 的映射”这一最佳实践。
static getDerivedStateFromProps(props, state)
确保生命周期函数的行为更加可控可预测,从根源上帮开发者避免不合理的编程方式,避免生命周期的滥用;同时,也是在为新的 Fiber 架构铺路。
getDerivedStateFromProps 直接被定义为 static 方法 —— static 方法内部拿不到组件实例的 this,这就导致你无法在 getDerivedStateFromProps 里面做任何类似于 this.fetch()、不合理的 this.setState(会导致死循环的那种)这类可能会产生副作用的操作。
5. 消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate
getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息。
getSnapshotBeforeUpdate(prevProps, prevState) {
return ...
}
componentDidUpdate(preProps, preState, valueFromSnapshot) {
console.log("componentDidUpdate方法执行");
console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot);
}
为什么 componentWillUpdate 就非死不可呢?说到底,还是因为它“挡了 Fiber 的路”。
三、Fiber 架构简介
Fiber 会使原本同步的渲染过程变成异步的。
1. React16之前
在 React 16 之前,每当我们触发一次组件的更新,React 都会构建一棵新的虚拟 DOM 树,通过与上一次的虚拟 DOM 树进行 diff,实现对 DOM 的定向更新。这个过程,是一个递归的过程。
同步渲染的递归调用栈是非常深的,只有最底层的调用返回了,整个渲染过程才会开始逐层返回。这个漫长且不可打断的更新过程,将会带来用户体验层面的巨大风险:同步渲染一旦开始,便会牢牢抓住主线程不放,直到递归彻底完成。在这个过程中,浏览器没有办法处理任何渲染之外的事情,会进入一种无法处理用户交互的状态。因此若渲染时间稍微长一点,页面就会面临卡顿甚至卡死的风险。
2. React 16 引入的 Fiber 架构
Fiber 会将一个大的更新任务拆解为许多个小任务。每当执行完一个小任务时,渲染线程都会把主线程交回去,看看有没有优先级更高的工作要处理,确保不会出现其他任务被“饿死”的情况,进而避免同步渲染带来的卡顿。在这个过程中,渲染线程不再“一去不回头”,而是可以被打断的,这就是所谓的“异步渲染”。
- render 阶段:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。
- pre-commit 阶段:可以读取 DOM。
- commit 阶段:可以使用 DOM,运行副作用,安排更新。
总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。
由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,用户是可见的。
四、废旧立新的思考
在 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)即使你没有开启异步,React 15 下也有不少人能把自己“玩死”。
比如在 componentWillReceiveProps 和 componentWillUpdate 里滥用 setState 导致重复渲染死循环。
五、结论
React 16 改造生命周期的主要动机是为了配合 Fiber 架构带来的异步渲染机制。
针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践。这一系列的工作做下来,首先是确保了 Fiber 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。
六、问与答
1、问:当 props 不变的前提下, PureComponent 组件能否阻止 componentWillReceiveProps 执行?
答案是否定的,componentWillReceiveProps 生命周期的执行,和纯组件没有关系,纯组件是在 componentWillReceiveProps 执行之后浅比较 props 是否发生变化。所以 PureComponent 下不会阻止该生命周期的执行。
2、问:React.useEffect 回调函数 和 componentDidMount / componentDidUpdate 执行时机有什么区别 ?
答:useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。