React生命周期
在之前的React版本中,如果有一个很复杂的复合组件,然后改动了最上层组件的 state,那么调用栈可能会很长,再加上中间进行了复杂的操作,可能导致长时间阻塞主线程,带来不好的用户体验。React Fiber应运而生。
Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。
React 如何区别优先级: 对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的情况下,React 会每 16 ms(以内) 暂停一下更新,返回来继续渲染动画。
异步渲染有两个阶段:reconciliation(调和阶段) 和 commit(提交),前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。
组件的生命周期可分成三个状态:
- Mounting:已插入真实 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实 DOM
生命周期方法
Constructor()
React借用class类的constructor充当初始化钩子, 因为我们只允许通过特定的途径给组件传递参数,所以constructor的参数实际上是被React规定好的。React规定constructor有三个参数,分别是props、context和updater。
- props是属性,它是不可变的。
- context是全局上下文。
- updater是包含一些更新方法的对象
this.setState最终调用的是this.updater.enqueueSetState方法,this.forceUpdate最终调用的是this.updater.enqueueForceUpdate方法,所以这些API更多是React内部使用,暴露出来是以备开发者不时之需。
在React中,因为所有class组件都要继承自Component类或者PureComponent类,因此和原生class写法一样,要在constructor里首先调用super方法,才能获得this。constructor生命周期钩子的最佳实践是在这里初始化this.state。
当然,你也可以使用属性初始化器来代替,如下:
import React, { Component } from 'react';
class App extends Component {
state = {name: 'biu',};
}
export default App;setState会怎样?(我说怎么会在这里setState,面试官说如果就是有人这么手欠呢?hhh)
setState是无效的,setState(...): 仅更新mounted 或者 mounting的组件,在constructor中,组件还没有mounted/mounting ,所以会报错,被忽略掉这里的setState。
Reconciliation阶段(可打断)
componentWillMount()
在组件挂载到DOM之前(渲染前)调用, 在客户端也在服务端(server-side-rendering服务端渲染SSR)
不在这里请求数据:componentWillMount和挂载是同步执行的,意味着执行完这个钩子,立即挂载。而向服务器请求数据是异步执行的。所以无论请求怎么快,都要排在同步任务之后再处理,也就是说,永远不可能在这里将请求的数据插入元素一同挂载。(在didMount里面调用异步ajax请求)
componentWillReceiveProps(newProps)
在组件接收到新的 prop时被调用, 初始化不触发。
- 组件接收到了新的属性。
- 组件没有收到新的属性,但是由于父组件重新渲染导致当前组件也被重新渲染。
shouldComponentUpdate (nextProps, nextState)
返回一个布尔值,判断是否更新,用来优化性能(默认返回true)。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。可以在你确认不需要更新组件时使用。(PureComponent自动实现了浅比较的shouldComponentUpdate())
注:可以用在比较少进行修改的组件上,因为如果如果一个组件比较常修改,就不用浪费时间去自己判断了,比如说 父组件里setstate之后 会调用父组件的render 子组件也都要更新 但是有些子组件可能没有用到父组件的状态 或者是一部分很少改变的状态,就调用shouldComponentUpdate设置让他不要更新
componentWillUpdate(nextProps, nextState)
在组件接收到新的props或者state(shouldComponentUpdate返回true/调用this.forceUpdate()的时候)但还没有render时被调用,在初始化时不会被调用。(慎用setState(),可能无限循环)
注:除了shouldComponentUpdate,其他三个正在逐渐废弃,弄了个别名 UNSAFE_componentWillXXX
render()
构建抽象UI(要挂载到DOM上的元素),千万不要在render生命周期钩子里调用this.setState,因为this.setState会引发render,陷入无限循环。
Commit阶段(一直更新到完成)
componentDidMount()
在第一次渲染后调用,只在客户端。
使用render方法返回的虚拟DOM对象来创建真实的DOM结构, 之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求数据等操作。
为什么在这里请求数据:
请求得到的数据一般是要加载到对应的dom的,异步请求的结果在第一次渲染前还没有拿到,如果没有设置state的这个拿到的数据属性的初始值(constructor中this.state设置初始值/defaultProps设置这个子组件的初始props),会报map undefined的错误, 如果设置了初始值,也会用空的数据进行至少一次render。
在didmount这里进行请求数据,可以更加明确上述过程是:
先渲染了再进行的请求获取数据,根据拿到的数据setState再渲染一次加载数据到dom上。
不在willmount中请求数据是因为:
willmount正在被废弃,且由于fiber可能重复执行,服务端&客户端都会执行一次,didmount只在客户端执行。
componentDidUpdate (nextProps, nextState, snapshot)
在组件完成更新后立即调用。在初始化时不会被调用(慎用setState())
componentWillUnmount
在组件从 DOM 中移除之前立刻被调用。组件中用到的原生的事件监听器(react的合成事件会自己销毁)、订阅器、定时器都要在这里销毁
因为 Reconciliation 阶段是可以被打断的,所以 Reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。由此对于 Reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。
解决方法(引入的新方法)
getDerivedStateFromProps(nextProps, prevState)
用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用,根据父组件传来的props按需更新自己的state(衍生state)。返回的对象就是要增量更新的state。
它被设计成静态(static)方法, 这样开发者就访问不到this也就是实例了,也就不能在里面调用实例方法或者setsState, 目的是保持该方法的纯粹,它就是用来定义衍生state的,除此之外不应该在里面执行任何操作。
class ExampleComponent extends React.Component {
// Initialize state in constructor,
// Or with a property initializer.
state = {};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.someMirroredValue !== nextProps.someValue) {
return {
derivedData: computeDerivedState(nextProps),
someMirroredValue: nextProps.someValue};
}
// Return null to indicate no change to state.
return null;
}
}getSnapshotBeforeUpdate(prevProps, prevState)
用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。返回的值会被componentDidUpdate的第三个参数接收,我们可以利用这个通道保存一些不需要持久化的状态,用完即可舍弃,有一些状态,比如网页滚动位置,我不需要它持久化,只需要在组件更新以后能够恢复原来的位置即可。
componentDidCatch(err,info)
它主要用来捕获错误并进行相应处理,所以它的用法也比较特殊。
定制一个只有componentDidCatch生命周期钩子的ErrorBoundary组件,它只做一件事:如果捕获到错误,则显示错误提示,如果没有捕获到错误,则显示子组件。将需要捕获错误的组件作为ErrorBoundary的子组件渲染,一旦子组件抛出错误,整个应用依然不会崩溃,而是被ErrorBoundary捕获。
参考资料: