先看旧生命周期的整体流程图
首先要注意,只有类组件有生命周期,函数式组件只能通过hooks实现类似的功能。
1.初始化阶段
1.1 componentWillMount
该函数在react16.2后已被建议弃用。因为老的diff算法在对比虚拟DOM和真实DOM时是同步的,万一对比量过大容易造成性能影响。
16.2推出了新的生命周期,更新了diff算法,提出了fiber技术。此时componentWillMount属于低优先级任务,容易被高优先级任务中断,需要重新执行,具有隐患。
第一次render之前最后一次修改状态的机会,主要作用是进行数据初始化。
1.2 render
只能访问this.props和this.state,不允许修改状态和dom输出
1.3 componentDidMount
成功render并渲染完成真实dom后触发,可以修改dom。
适合用于
- 数据请求 比如axios
- 订阅函数调用
- setInterval
- 基于创建完的dom进行初始化
来看个案例,BetterScroll需要基于创建完的DOM来工作,因此写在componentDidMount中。
- export default class App extends Component {
- state = {
- list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
- }
- UNSAFE_componentWillMount() {
- console.log(document.querySelectorAll('li'))//获取到一个空数组
- }
- render() {
- return (
- <div>
- <div id='wrapper' style={{height:'150px',background:'lightgreen',overflow:'hidden'}}>
- <ul>
- {
- this.state.list.map(item =>
- <li key={item}>{item}</li>
- )
- }
- </ul>
- </div>
- </div>
- )
- }
- componentDidMount() {
- console.log(document.querySelectorAll('li'))//获取到10个li
- // 要在这里new 因为此时已经获取到dom节点
- new BetterScroll('#wrapper')
- }
- }
2. 运行(更新)阶段
2.1 componentWillUpdate
用的机会不多,不像Mount可以用来初始化数据,在Update阶段初始化数据会导致死循环。(render后又update状态->状态修改后又触发render...)
类似于componentWillMount,也是不安全的,建议弃用。
2.2 render
和初始化阶段一样,只能访问this.props和this.state,不允许修改状态和dom输出
2.3 componentDidUpdate
类似componentDidMount,作用是更新后用来获取DOM节点
看个小例子,在componentDidMount中用axios异步获取数据时需要将betterscroll写在setState的第二个参数中作为回调函数执行。
- componentDidMount() {
- axios.get(`/test.json`).then(res => {
- this.setState({
- filmList: res.data.data.films
- }, () => {
- new BetterScroll('#wrapper')
- })
- })
- }
可以不在setState中写,直接在axios获取到数据并更新状态后的componentDidUpdate中写,效果相同。
componentDidUpdate也有对应的缺点,就是会执行多次,每次更新状态都会使得它重新执行。
- componentDidMount() {
- axios.get(`/test.json`).then(res => {
- this.setState({
- filmList: res.data.data.films
- })
- })
- }
- componentDidUpdate() {
- new BetterScroll('#wrapper')
- }
该生命周期可以传入两个参数,用于记录老的属性和老的状态
- componentDidUpdate(prevProps , prevState) {
- console.log(prevState.filmList) //空数组[]
- if(prevState.filmList.length === 0){
- new BetterScroll('#wrapper')
- }
- }
2.4 shouldComponentUpdate
如果该方法返回false,那么就会阻止render调用
举个例子,第一次点击按钮后myname会改变,但是之后每次点击按钮,虽然state值没有改变,但还是会继续触发虚拟dom和真实dom的对比,render重新渲染页面。
- export default class App extends Component {
- state = {
- myname: 'yoimiya',
- }
- render() {
- return (
- <div>
- <button onClick={() => {
- this.setState({
- myname: 'eula'
- })
- }}>click</button>
- <span id='myname'>{this.state.myname}</span>
- </div>
- )
- }
- }
该生命周期也是可以传两个参数,分别代表新属性和新状态
- // this.state 老的状态
- // nextState 新的状态
- shouldComponentUpdate(nextProps , nextState){
- if(this.state.myname !== nextState.myname){
- // 允许render调用
- return true;
- }else{
- // 不允许render调用
- return false;
- }
- }
为了不一一对比对象中的属性,直接将对象序列化后对比两个整体对象。
- // this.state 老的状态
- // nextState 新的状态
- shouldComponentUpdate(nextProps , nextState){
- if(JSON.stringify(this.state) !== JSON.stringify(nextState)){
- return true;
- }else{
- return false;
- }
- }
该生命周期对于性能优化有着重要的作用,举个例子:一个父组件底下10个子组件,根据父组件状态修改其中1个子组件会导致剩下的也要重新渲染,大大影响性能。
先来看没用shouldComponentUpdate优化的代码,根据输入框的value修改current状态,将current和index传给子组件做对比,相等时背景色改变:
- class Box extends Component {
- render() {
- return (
- <div style={{
- width: '100px', height: '100px', border: '1px solid grey', margin: '10px', float: 'left',
- // 根据条件来决定背景色
- background: this.props.current === this.props.index ? 'lightgreen' : ''
- }}>
- </div>
- )
- }
- }
- export default class App extends Component {
- state = {
- list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
- current: 0
- }
- render() {
- return (
- <div>
- {/* 根据输入框内容修改current 记得用Number()转换 */}
- <input type='number' onChange={(e) =>{
- this.setState({
- current : Number(e.target.value)
- })
- }}></input>
- <div style={{ overflow: 'hidden' }}>
- {
- this.state.list.map((item, index) =>
- // 将current和index传给子组件
- <Box key={item} current={this.state.current} index={index}></Box>
- )
- }
- </div>
- </div>
- )
- }
- }
但是这份代码有个问题,每次修改current,10个Box都会被render重新渲染一次,造成性能影响。
因此可以在Box组件里加入这段代码,实现了每次render只更新2个(老的和新的)需要被更新的Box:
- shouldComponentUpdate(newProps){
- // 只允许render更新老的Box:选中->未选中,和新的Box:未选中->选中
- if(this.props.current === this.props.index || newProps.current === newProps.index){
- return true;
- }
- return false;
- }
2.5 componentWillReceiveProps
此生命周期也已经被弃用
父组件修改属性时触发,因此应该写在子组件内。
但是其实并不需要将属性传给孩子,哪怕只是在父组件中更新状态,也会导致子组件的生命周期更新,也会触发该方法。
这个生命周期唯一作用是最先获得父组件传来的属性,可以利用属性进行相应的操作
通过形参中的nextProps(名字不固定),来获取最新的属性
比如这个例子
- class Child extends Component {
- render() {
- return (
- <div>
- child
- </div>
- )
- }
- componentWillReceiveProps(nextProps){
- console.log(this.props.text , nextProps.text)
- // 111 222
- }
- }
- export default class App extends Component {
- state = {
- text : '111'
- }
- render() {
- return (
- <div>
- {
- this.state.text
- }
- <button onClick={() =>{
- this.setState({
- text : '222'
- })
- }}>click</button>
- <Child text={this.state.text}></Child>
- </div>
- )
- }
- }
3. 销毁阶段
销毁阶段就一个函数:componentWillUnmount
作用是在移除组件之前进行清理操作,比如计时器和事件监听器。
- export default class App extends Component {
- state = {
- isCreated: true
- }
- render() {
- return (
- <div>
- <button onClick={() => {
- this.setState({
- isCreated: !this.state.isCreated
- })
- }}>click</button>
- {this.state.isCreated && <Child />}
- </div>
- )
- }
- }
- class Child extends Component {
- render() {
- return <div>
- child
- </div>
- }
- componentDidMount() {
- window.onresize = () => {
- console.log("resize")
- }
- // this是Child组件的一个实例
- this.timer = setInterval(() => {
- console.log("111")
- }, 1000)
- }
- componentWillUnmount() {
- console.log("componentWillUnmount")
- // 清除掉初始化时设置的监听事件
- window.onresize = null
- // 清除掉定时器
- clearInterval(this.timer)
- }
- }
4. 新生命周期
先看整体流程图
注意react16自上而下对生命周期做了另一种解读,render阶段,pre-commit阶段还有commit阶段
4.1 老生命周期的问题
(1) componentWillMount
在ssr中这个方法将会被多次调用,所以会重复触发多遍,同时在这里如果绑定事件将无法解绑,导致内存泄漏,变得不够安全高效逐步废弃。(还有上面提到的fiber算法导致低优先级的原因)
(2) componentWillReceiveProps
- 外部组件多次频繁更新传入多次不同的 props,会导致不必要的异步请求
- 旧的React中componentWillReceiveProps方法是用来判断前后两个props是否相同,如果不同,则将新的props更新到相应的state上去。在这个过程中我们实际上是可以访问到当前props的,这样我们可能会对this.props做一些奇奇怪怪的操作,很可能会破坏state数据的单一数据源,导致组件状态变得不可预测。
(3) componetWillupdate
在React开启异步渲染模式后,在执行函数时读到的DOM元素状态并不总是渲染时相同,这就导致在componentDidUpdate中使用componentWillUpdate中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
4.2 getDerivedStateFromProps
第一次初始化组件以及后续的更新过程中(包括自身状态更新以及父传子),返回一个对象作为新的state,返回null则说明不需要在这里更新state。
对比componentWilReceiveProps的优点在于禁止了组件去访问this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去访问this.props并做其他一些让组件自身状态变得更加不可预测的事情。
其次,它将所有的异步操作合并在一起返回一次state,避免了频繁传入props导致的不断异步请求引起的资源浪费。
下面来看看具体用法,体现出该函数在初始化时和后续更新时会被调用,可以通过nextState获取新状态,有两个注意点:
- export default class App extends Component {
- state = {
- myname: 'yoimiya'
- }
- // // (1)需要写成静态方法(要注意静态方法里没有自己的this)
- static getDerivedStateFromProps(nextProps , nextState) {
- console.log(nextState) //初始为{myname:'yoimiya} 点击后为{myname:'eula}
- // (2)需要返回一个值作为新的state 可以为空 代表状态不变
- return {
- myname: nextState.myname.substring(0,1).toUpperCase() + nextState.myname.substring(1)
- }
- }
- render() {
- return (
- <div>
- <button onClick={() => {
- this.setState({
- myname: 'eula'
- })
- }}>click</button>
- {this.state.myname}
- </div>
- )
- }
- }}
在实际运用中,建议该生命周期只负责将传过来的props转化为自己的state,需要搭配componentDidUpdate()来执行。
具体代码看自己vscode里的文件吧('./03_lifecycle/37_gdsfpDemo') 太长了,其实就是把componentWillReceiveProps替换成getDerivedStateFromProps + componentDidUpdate。
4.3 getSnapshotBeforeUpdate
取代了componetWillUpdate,解决了willUpdate和didUpdate之间时间间隔过长的问题。
触发时间为update发生的时候,在render之后didUpdate之前返回一个值,作为componentDidUpdate的第三个参数。
来看基本用法
- export default class App extends Component {
- state = {
- text: 111
- }
- render() {
- console.log('render')
- return (
- <div>
- <button onClick={() =>{
- this.setState({
- text: 222
- })
- }}>click</button>
- {this.state.text}
- </div>
- )
- }
- getSnapshotBeforeUpdate(){
- console.log('snapshot');
- // 必须得return点东西 作为参数传给DidUpdate
- return 100;
- }
- componentDidUpdate(prevProps , prevState , value){
- console.log('did update' , value)
- }
- //render
- //render
- //snapshot
- //did update 100
- }
来看个具体的案例,邮箱来邮件之后让视窗仍然保持在当前在看的邮件,而非自动弹到最新邮件处。
- export default class App extends Component {
- state = {
- list: [1, 2, 3, 4, 5]
- }
- myRef = React.createRef();
- render() {
- return (
- <div>
- <h1>邮箱Demo</h1>
- <div style={{ height: '200px', overflow: 'auto' }} ref={this.myRef}>
- <ul>
- {
- this.state.list.map(item =>
- <li key={item} style={{ height: '100px', background: 'lightgreen' }}>{item}</li>
- )
- }
- </ul>
- </div>
- <button onClick={() => {
- this.setState({
- list: [...[11, 12, 13, 14, 15], ...this.state.list]
- })
- }}>收邮件</button>
- </div>
- )
- }
- getSnapshotBeforeUpdate() {
- //获取未更新dom前容器高度
- return this.myRef.current.scrollHeight
- }
- componentDidUpdate(prevProps , prevState , value){
- // 将top值设为滚动高度差 成功实现“快照保存”邮件来前位置效果
- this.myRef.current.scrollTop += this.myRef.current.scrollHeight - value;
- }
- }
5.PureComponent
顺带一提这个性能优化的问题,之前的shouldComponentUpdate需要自己去判断状态,属性等来决定组件的更新。
通过继承PureComponent可以通过一种叫shallowEqual的方法直接实现自动比较,也就是react帮你把活干了。
该方法可以应用于绝大部分场景,除非state或props一直会改变(比如倒计时),那么pureComponent就显得没有必要去比较新老状态是否改变了,反而会因为shallowEqual影响性能。
使用方法是直接继承即可
- // 注意这里的导入
- import React, { PureComponent } from 'react'
- export default class App extends PureComponent {
- state = {
- myname: 'yoimiya',
- }
- render() {
- return (
- <div>
- <button onClick={() => {
- this.setState({
- myname: 'eula'
- })
- }}>click</button>
- <span id='myname'>{this.state.myname}</span>
- </div>
- )
- }
- // this.state 老的状态
- // nextState 新的状态
- // shouldComponentUpdate(nextProps, nextState) {
- // if (JSON.stringify(this.state) !== JSON.stringify(nextState)) {
- // return true;
- // } else {
- // return false;
- // }
- // }
- }
使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。
pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。