React的setState你真的用对了吗?

121 篇文章 7 订阅
11 篇文章 1 订阅

 首先我们要明确一点:setState并不是一个异步方法,很像异步的原因:hook()与合成事件的调用顺序与setState的执行顺序存在偏差,最终产生异步的展示形式,从而造成异步的假象。记录setState必然要在各生命周期中执行,因此先引入生命周期的概念,第3部分开始详细记录开发中setState如何对数据进行同步异步操作的一些问题。


分享每日一题:题目传送门

前端电子书大全:电子书

1、生命周期

在React中,生命周期的地位毋庸置疑,熟练掌握了生命周期,对编写低冗余高性能的组件具有极大帮助。首先介绍下react16版本之后的生命周期,虽然最新更新内容应用场景不多,但还是很有必要了解下(借用程墨大神的图)

1.1、初始化阶段(Initialization)

在初始化阶段我们通常使用构造器进行props与state的准备,这里记录两种形式的初始化,第一种是基于基类Component的PageInit类的介绍,第二种是不基于基类的Listener类的介绍。这两种类的区别很简单,就是存在能否使用生命周期的差异,基于Component的类可以使用render,CDM,CDU等生命周期,而Listener类不具备使用生命周期的条件。

基于基类的PageInit类:

其中super()用来调用基类react.Component中的构造方法constructor(),同时将父组件的props注入到子组件(props只读),在子组件中props便存入内部state中用以数据的读写操作(state可变),constructor()则用来初始化组件的部分功能,同时预挂载某些函数,如下所示:

class PageInit extends react.Component {//基于基类的**类
    constructor(props) {
        super(props);
        this.state = {
            current: 1
        };
        this.IsPage = false;
    }
}

不基于基类的Listener类:

这种类最突出的特点就是不需要调用基类的构造方法,避免extends React.Component中,一旦调用基类方法,其中construcor中的state便不再是调用处注入的state,而变成的基类传过来的props。调用处方法:

let mapListenerToStore = new listener(this.mapObj);

只要在基于基类的组件中调用了listener,便把其中的对象注入到监听器中,因此监听器与扩展类便可共享同一对象,注入删除数据也变为同步的了。

class listener {
    constructor(state){//state 为注入的this.mapObj
        this.mapObj=state;
        this.newfunction01();
        this.newfunction02();
        this.newfunction03();
        state.map.on('click',this.clickSouth);
        this.clickSouth=null;
    }
}

1.2、挂载阶段(mount)

componentWillMount()

作用与constructor的作用有些相似,相当于预挂载的内容。此生命周期为组件挂载到DOM之前调用一次,在本周期中调用this.setState方法不会引起页面的重渲染,很少见

componentWillMount(){
       ..............
}

render()

作用是根据自身参数props与state的改变重渲染页面的DOM,render作为一个纯函数(一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用)非常适合判断自身参数的改变,假如props与state有变化,则能够第一时间对页面进行重渲染。因此我们不可以在render周期中进行this.setState方法的调用,调用后会产生不间断渲染页面的后果。

关于纯函数,可以参考这篇文章,这位大触讲解的非常清楚:

JavaScript----什么是纯函数icon-default.png?t=L9C2https://blog.csdn.net/weixin_42232156/article/details/120882206?spm=1001.2014.3001.5501

render(){
    return(
     <div>***</div>
)
}
componentDidMount( )

组件挂载到DOM后调用该周期,全程只执行一次。需要注意的是本组件的render执行完,并不会调用componentDidMount(),而是全部子组件的render调用完后才会调用CDM周期。

1.3、更新阶段

更新阶段的实现较为复杂,其中包含了componentWillReciveProps>shouldComponentUpdatate>ComponentWillUpdate>render>componentDidUpdate这五个基本周期,render作为纯函数在更新阶段只是被调用。个人认为更新阶段只需要了解其根据props与state的更改比较机制即可。且setState方法在该阶段不可在hook内注入,强行注入只会导致组件的不停渲染。具体如下:

componentWillReciveProps(nextProps)

本周期只在props改变后将要引起组件的重新渲染时才调用,传入的参数(nextProps)为父组件传递给子组件的新props,但前面也提到,render方法仅仅为一个纯函数,不会对state或props的值是否改变进行比较,只要传入相应的props或是state就会引起组件的重渲染,所以要通过本作用域中的this.state与传过来的nextProps进行比较来得出是否要重新调用render进行渲染,所以本生命周期可以有效减少无效渲染次数,并提高周期的开发效率

shouldComponentUpdate(nextProps,nextState)

本周期通过比较当前组件的this,props/this.state和传入的nextProps/nextState,进行是否需要重渲染的判断,未发生改变返回false,当前组件的更新停止。若组件的state与props发生改变则返回true,当前组件的更新开始。通过比较同样可以减少组件不必要的渲染,优化组件性能

ComponentWillUpdate(nextProps,nextState)

本周期在调用render之前的预渲染,在开发中还未用到过,原理同样是比较当前组件的this.props/this,state和传入的参数nextProps/nextState的差异性。存在差异则渲染。

render

render周期仅被调用

componentDidUpdate(preProps,preState)

本周期进行渲染后与DOM的操作,传入的参数为preProps/preState,参数均为组件更新前的props和state

需要重点注意的是,更新阶段内不许使用setState方法进行state值的修改,否则会引起render的循环重渲染

造成组件更新的原因:

1、父组件render周期重渲染

情况1:在父组件中无论props的值是否改变,render都会重新渲染,因此造成组件更新。这种问题可以通过更新周期中的shouldComponentUpdate对组件自身的props与其父元素传递的props进行比较

class Child extends Component {
   shouldComponentUpdate(nextProps){ 
        if(nextProps.someThings === this.props.someThings){
          return false
        }
    }
    render() {
        return <div>{this.props.someThings}</div>
    }
}

情况2:在子组件中将接收到的props转换为自身的state,会造成render重渲染。

class Child extends Component {
    constructor(props) {
        super(props);
        this.state = {
            someThings: props.someThings
        };
    }
    render() {
        return <div>{this.state.someThings}</div>
    }
}

我们可以通过componentWillReciveProps方法进行修改,上述代码改为:

class Child extends Component {
    constructor(props) {
        super(props);
        this.state = {
            someThings:null
        };
    }
    componentWillReceiveProps(nextProps) { // 父组件重传props时就会调用这个方法
        this.setState({someThings: nextProps.someThings});
    }
    render() {
        return <div>{this.state.someThings}</div>
    }
}

就不会引起冗余的渲染。

2、本组件进行setState方法的调用

一般情况:在componentDidMount周期调用的函数中进行setState方法的调用,且赋值与state内容不同。

特殊情况:state未发生变化还是会进行重渲染

class Child extends Component {
   constructor(props) {
        super(props);
        this.state = {
          someThings:1
        }
   }

   handleClick = () => { // 虽然调用了setState ,但state并无变化还是为someThings:1
        const preSomeThings = this.state.someThings
         this.setState({
            someThings: preSomeThings
         })
   }

    render() {
        return <div onClick = {this.handleClick}>{this.state.someThings}</div>
    }
}

调用shouldComponentUpdate周期进行state是否变化的判断,在handleclick前添加如下代码:

  shouldComponentUpdate(nextStates){ // 应该使用这个方法,否则无论state是否有变化都将会导致组件重新渲染
        if(nextStates.someThings === this.state.someThings){
          return false
        }
    }

到这里组件的更新周期、哪些情况下会更新记录完毕。

1.4、卸载周期

此阶段只有一个生命周期方法:componentWillUnmount

此方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清除组件中使用的定时器,清楚componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。

常用周期记录完毕

参考文章:详解React生命周期(包括react16版)

2、React16.4后新增周期

借用程墨大神的周期图:

 

新引入了两个新的生命周期函数:getDerivedStateFromPropsgetSnapshotBeforeUpdate

因为在开发中还没有用到,当用到后再进行记录,(所有文章都在开发经验的迭代中持续更新)附上大神的总结用以学习:

React v16.3之后的组件生命周期函数icon-default.png?t=L9C2https://blog.csdn.net/weixin_42232156/article/details/120882627?spm=1001.2014.3001.5501

组件的生命周期记录结束

3、setState进行异步数据处理

例a:在日常的开发中我们通常是在ComponentDidMount周期中使用setState方法,CDM周期的特点使setState变得容易理解。详情如下

class App extends Component {
    state = {
        ante: 0
    }
​
    componentDidMount(){
        this.setState({
           ante: 1
         })
        console.log(this.state.ante) // 0
    }
​
    render(){
        ...
    }
}

例b:但是在CDM的特点,仅仅在render执行完后执行一次。无论如何我们在这个周期中也获取不到更新完的state,但是react架构的开发人员在其中提供了一个回调去实现更改后的state值的获取:

CDM(){
  this.setState({
  ante:1
},()=>{//通过回调实现
   console.log(this.state.ante)//1
})

}

 例c:输出得到的结果变为我们想要的1,原理:回调函数的执行在state更新后,可以立即获取。但是又出现了这样一个问题

state = {
        ante: 0
    }

CDM(){
  this.setState({
  ante:this.state.ante+1
},()=>{//通过回调实现
   console.log(this.state.ante)//1
})

  this.setState({
  ante:this.state.ante+1
},()=>{//通过回调实现
   console.log(this.state.ante)//1
})

}

例d:多次调用setState方法后两次打印出的state值均为1,原因是:回调函数仅仅可以获取到当前修改后的state,再次执行setState方法获取到的state值依然为挂载后初始state值。因此解决办法为 

state = {
        ante: 0
    }

ComponentDidMount(){
  this.setState(preState=>{
  ante:preState.ante+1
},()=>{//通过回调实现
   console.log(this.state.ante)//1
})

  this.setState(preState=>{
  ante:preState.ante+1
},()=>{//通过回调实现
   console.log(this.state.ante)//2
})

}

这么做可以实现在不同的setState方法中对初始state的不断累加,原理是setState本身可以接受函数作为参数,而在这里我们使用的参数就是上一次的state。检测自己是否真正掌握了异步操作数据的setState方法了呢?下面例子来试一试:

state = {
        ante: 0
    }
ComponentDidMount(){ 
    this.setState({ 
      ante: this.state.ante+ 1 });
    console.log("console: " + this.state.ante); // 输出1

    this.setState({ ante: this.state.ante+ 1 }, () => {
      console.log("console from callback: " + this.state.ante); // 输出2
    });

    this.setState(prevState => {
      console.log("console from func: " + prevState.ante); // 输出3
      return {
        ante: prevState.ante+ 1
      };
    }, ()=>{
      console.log('last console: '+ this.state.ante)//输出4
    });
}

输出结果及执行顺序:

输出1:console:0
输出3:console from func:1
输出2:console from callback:2
输出4:last console:2

如果没有出错,那么恭喜你对state的执行队列已经了然于胸了。假如存在疑问,那需要知道state是具有更新队列的,每次调用setState方法都会将当前修改的state放入此队列,react查询到当前setState方法执行完后,进行state数据的合并,合并后再去执行回调,根据合并结果再去执行更新VirtualDom,触发render周期

4、setState异步设计原理及应用场景

我们只需要了解一点,异步的作用是提高性能,降低冗余。简单说,因为state具有更新队列,将所有更新都累计到最后进行批量合并再去渲染可以极大提高应用的性能,像源生JS那样修改后就进行DOM的重渲染,会造成巨大的性能消耗。同样这与react优化后的diff算法也有关系。后期将详细记录react优化后的diff算法。官方解释在下方:

  https://github.com/facebook/react/issues/11527#issuecomment-360199710https://github.com/facebook/react/issues/11527#issuecomment-360199710icon-default.png?t=L9C2https://github.com/facebook/react/issues/11527#issuecomment-360199710

5、setState的本来面目(同步应用场景)

react为了解决跨平台等问题,自己在JSX中封装了一套事件机制,代理了原先的源生事件,像是在JSX中render内返回的dom中添加的onClick,onFocus等方法都是合成事件。所以setState并不是真正意义的异步操作,仅仅是模拟了异步的行为。实现是通过isBatchingUpdates来判断是先存进state队列还是直接更新。值为true则执行异步操作,false则直接更新,典型例子为使用setTimeout定时器时,则直接使用同步操作:

state={
count:0
}

componentDidMount() {
    // 生命周期中调用
    this.setState({ count: this.state.count + 1 });
    console.log("lifecycle: " + this.state.count);//0

    setTimeout(() => {
      // setTimeout中调用
      this.setState({ count: this.state.count + 1 });
      console.log("setTimeout: " + this.state.count);//1,延时设置为0立即执行
    }, 0);
    document.getElementById("div2").addEventListener("click", this.increment2);
  }
​
  increment = () => {
    // 合成事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("react event: " + this.state.count);
  };
​
  increment2 = () => {
    // 原生事件中调用
    this.setState({ count: this.state.count + 1 });
    console.log("dom event: " + this.state.count);//点一下加一,一直加
  };
render() {
    return (
      <div className="App">
        <h2>couont: {this.state.count}</h2>
        <div id="div1" onClick={this.increment}>
          click me and count+1
        </div>
        <div id="div2">click me and count+1</div>
      </div>
    );
  }
}

 setState异步、同步与进阶icon-default.png?t=L9C2https://blog.csdn.net/weixin_42232156/article/details/120883453

在真实开发中不推荐使用合成方法与源生方法混用的情况。

6、总结

a:setState 只在合成事件和hook()中是“异步”的,在原生事件和 setTimeout 中都是同步的。


b: setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和hook()的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 拿到更新后的结果。


c: setState 的批量更新优化也是建立在“异步”(合成事件、hook())之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新

reference:你真的理解setState吗?

6、如何在setState后获取最新的state数据 

/Setstate有可以有两个参数,第一个参数又有两种方式,第一种方式是对象{}/
constructor(props) {
   super(props);
   this.state = {
     msg:false
   }
 }


setdata(){
   this.setState({
     msg:!this.state.msg
   }
 }
 /第二种方式是一个方法/
setdata() {
   this.setState(() => {
     return {
       msg: !this.state.msg
     }
   })
 }
 /第二种方式也可以这样获取/
   this.setState((prevState,props) => {
   console.log(prevState,props) //这里拿到的是state旧的值false
     return {
        msg: prevState.msg
     }
  /使用第二个参数 是个方法输出相应的内容/
 setdata() {
   this.setState(() => {
     return {
       msg: !this.state.msg
     }
   }, () => {
     console.log(this.state.msg)//这里拿到的是最新的值true
   })
 }

Happy Hacking~~~

推荐阅读: 

前端面试题拿到百度京东offer的,前端面试题2021及答案

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

短暂又灿烂的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值