setState

在以下情况执行setState方法:

1生命周期里:此时已经开启了事务(没有同步更新组件),当执行多个setState时,所有都是在脏数组中,意味着此时组件上的state没有更新。事务结束后批量更新。

2合成事件回调函数里:下发事件时开启了事务,回调函数里执行setState都是放在脏数组中,同上

3setTimeout和DOM原生事件里,此时没有开启事务,直接同步更新组件修改为最新的this.state。

4.异步操作:由于JS异步机制,异步操作之前钩子函数已经执行完(把isBatchingUpdates恢复为false),表现为同步。


哪些生命周期不能用setState:

在componentDidMount中执行setState会导致组件在初始化的时候就触发了更新,渲染了两遍,应尽量避免。有一些场景,比如在组件DOM渲染完成后获得DOM元素位置或者宽高等等设置为state,会不得在componentDidMount之后setState,但是除了这些必要的时候,都应该尽量避免在 componentDidMount里setState。

在componentDidUpdate中执行setState会导致组件刚刚完成更新,又要再更新一次,连续渲染两遍(和在componentDidMount中执行setState类似)。因此如非必须,应该尽量避免。

禁止在shouldComponentUpdate中调用setState,这会造成循环调用,直至耗光浏览器内存后崩溃。了解了生命周期之后,这条很好理解。在shouldComponentUpdate里调用setState会再次触发这个函数,然后这个函数又触发了setState,然后再次触发这个函数……这样就进入了一个不停 setState然后不停触发组件更新的死循环里,会导致浏览器内存耗光然后崩溃。


render()

Calling setState() here makes your component a contender for producing infinite loops.

The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser.

In this case, avoid using setState() here.

constructor()

You should not call setState() in the constructor(). Instead, if your component needs to use local state, assign the initial state to this.state directly in the constructor.

componentDidMount()

componentDidMount() is invoked immediately after a component is mounted. You may call setState() immediately in componentDidMount(). It will trigger an extra rendering, but it will happen before the browser updates the screen thus render() will be called twice.

componentDidUpdate()

componentDidUpdate() is invoked immediately after updating occurs. You may call setState() immediately here, but know that it must be wrapped in a condition like in the example below, or you’ll cause an infinite loop:

componentDidUpdate(prevProps, prevState) {  

    let newName = 'Obaseki Nosa'  

    //Don't forget to compare states  

    if(prevState && prevState.name !== newName) {    

        this.setState({name: newName});

    }

}


同步 or 异步

setState并不是异步的,是为了性能优化进行的批量更新。

由React引发的事件处理(如通过onClick引发的合成事件处理)和组件生命周期函数内(如componentDidMount),调用this.setState不会同步更新this.state;除此之外的setState调用会同步执行this.state,“除此之外”指的是绕过React通过addEventListener直接添加的事件处理函数,和通过setTimeout/setInterval产生的异步调用。

为什么会这样?

setState函数实现中,会根据变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说。isBatchingUpdates默认是false,表示setState会同步更新this.state。

React在调用事件处理函数和生命周期之前就会调用batchedUpdates,batchedUpdates函数会把isBatchingUpdates修改为true,后果就是由React控制的事件处理过程和生命周期中的同步代码调用的setState不会同步更新this.state


注意:在合成事件和生命周期内的异步调用setState(比如ajax和setTimeout内)会同步更新this.setState。

正常React用法都是会经过batchingUpdates方法的,这是由于React有一套自定义的事件系统和生命周期流程控制,使用原生事件监听和settimeout这种方式会跳出React这个体系,所以会直接更新this.state。

看代码是如何实现的,需要了解这样一个东西“事务”,React内部的工具方法实现了一个可供使用的事务。

setState_第1张图片
事务

React的事件系统和生命周期事务前后的钩子对isBatchingUpdates做了修改,在事务的前置pre内调用了batchedUpdates方法修改了变量为true,然后在后置钩子又置为false,然后发起真正的更新检测。事务中异步方法运行时候,由于JS的异步机制,异步方法(Ajax/setTimeout等)中的setState运行时候,同步的代码已经走完,后置钩子已经把isBatchingUpdates设为false,所以此时的setState会直接进入非批量更新模式,表现在我们看来成为了同步SetState。

尝试描述一下:整个React的每个生命周期和合成事件都处在一个大的事务当中。原生绑定事件和setTimeout异步的函数没有进入React的事务当中,或者是当他们执行时,刚刚的事务已经结束了(后置钩子触发了,close了)。

React“坐”在顶部调用堆栈框架并知道所有React事件处理程序何时运行,setState在React管理的合成事件和生命周期中调用,它会启用批量更新事务,进入了批量更新模式,所有的setState的改变都会暂存到一个队列,延迟到事务结束再合并更新。如果setState在React的批量更新事务外部或者之后调用,则会立即刷新。

懂得了事务,就明白,其实setState从来都是同步运行,不过是React利用事务工具方法模拟了setState异步的假象。

而在原生事件(不会执行pre钩子)异步操作中(异步操作之前执行了pre钩子,但是pos钩子也在异步操作之前执行完了),isBatchingUpdates必定为false,也就不会进行批量更新。


this.setState执行后干了什么?

this.setState首先会把state推入pendingState队列中。

然后将组建标记为dirtyComponent。

React中有事务的概念,最常见的就是更新事务,如果不在事务中,则会开启一次新的更新事务,更新事务执行的操作就是把组件标记为dirty。

判断是否处于batch update。

是的话,保存组建于dirtyComponent中,在事务的时候才会通过 ReactUpdates.flushBatchedUpdates 方法将所有的临时 state merge 并计算出最新的 props 及 state,然后将其批量执行,最后再关闭结束事务。

不是的话,直接开启一次新的更新事务,在标记为dirty之后,直接开始更新组件。因此当setState执行完毕后,组件就更新完毕了,所以会造成定时器同步更新的情况。


setState批量更新的过程

在react生命周期和合成事件执行前后都有相应的钩子,分别是pre钩子和post钩子,pre钩子调用batchedUpdate方法将isBatchingUpdates变量置为true,开启批量更新,post钩子会将isBatchingUpdates置为false。

isBatchingUpdates变量置为true,则会走批量更新分支,setState的更新会被存入队列中,待同步代码执行完后,再执行队列中的state更新。 isBatchingUpdates为true,则把当前组件(即调用了setState的组件)放入dirtyComponents数组中;否则batchUpdate所有队列中的更新



setState的执行原理可以分为两类:

1、批量更新:react内部的执行函数,执行setState都是批量更新处理,包括react合成事件和生命周期;

2、非批量更新:原生事件、setTimeout、fetch等;

两个概念:

1、事务:可以理解为,一个正常的函数外又被包裹了一层。这层包裹处理,包括一个或多个的函数执行前的处理函数(initialize函数),一个和多个函数执行后的处理函数(close函数)。React很多的逻辑处理,都使用了事务的概念。

2、合成事件和原生事件的关系和区别:

区别:原生事件是addEventListener写法的事件;合成事件是react中的onClick、onChange等;

关系:合成事件可以理解为react对原生事件的包裹封装;原生事件相当于上面事务概念中的正常的函数,而经过包装处理形成的事务,就是react中的合成事件。


接下来说明setState的内部执行逻辑:

事实上每个setState都是会直接触发render更新的。只是react经过内部处理,让它在一些情况下不会触发render(生命周期已说明);还有一些情况,就是同时存在多个setState的时候,不是每个setState都会触发,而是将state统一收集起来,进行统一render处理。

清晰了以上概念,接下来用合成事件和原生事件来对比说明,先举个栗子:

最后,总结一下setState:

1、setState的执行,分为两大类:一类是生命周期和合成函数;一类是非前面的两种情况;

2、两种类型下,setState都是同步执行,只是在批量更新类中,state和callback被收集起来延迟处理了,可以理解为数据的异步执行;而非批量更新类中的setState直接触发更新渲染。

3、callback与state同时收集,处理是在render之后,统一处理的。

二、生命周期函数中的setState

class App extends Component {

  state = { val: 0 }

componentDidMount() {

    this.setState({ val: this.state.val + 1 })

  console.log(this.state.val) // 输出的还是更新前的值 --> 0

}

  render() {

    return (

     

        {`Counter is: ${this.state.val}`}

     

    )

  }

}

四、setTimeout中的setState

class App extends Component {

  state = { val: 0 }

componentDidMount() {

    setTimeout(_ => {

      this.setState({ val: this.state.val + 1 })

      console.log(this.state.val) // 输出更新后的值 --> 1

    }, 0)

}

  render() {

    return (

     

        {`Counter is: ${this.state.val}`}

     

    )

  }

}

在setTimeout中去setState并不算是一个单独的场景,它是随着你外层去决定的,因为可以在合成事件中setTimeout,可以在钩子函数中setTimeout,也可以在原生事件setTimeout,但不管是哪个场景下,基于event loop的模型下,setTimeout中里去setState总能拿到最新的state值。

比如之前的合成事件,由于setTimeout(_ => { this.setState()}, 0)是在try代码块中,当try代码块执行到setTimeout的时候,把它丢到列队里,并没有去执行,而是先执行的finally代码块,等finally执行完了,isBatchingUpdates又变为了false,导致最后去执行队列里的setState时候,requestWork走的是和原生事件一样的expirationTime === Syncif分支,所以表现就会和原生事件一样,可以同步拿到最新的state值。

五、setState中的批量更新

class App extends Component {

  state = { val: 0 }

  batchUpdates = () => {

    this.setState({ val: this.state.val + 1 })

    this.setState({ val: this.state.val + 1 })

    this.setState({ val: this.state.val + 1 })

}

  render() {

    return (

     

        {`Counter is ${this.state.val}`} // 1

     

    )

  }

}

上面的结果最终是1,在setState的时候react内部会创建一个updateQueue,通过firstUpdate、lastUpdate、lastUpdate.next去维护一个更新的队列,在最终的performWork中,相同的key会被覆盖,只会对最后一次的setState进行更新


综合例子

class App extends React.Component {

  state = { val: 0 }

  componentDidMount() {

    this.setState({ val: this.state.val + 1 })

    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })

    console.log(this.state.val)

    setTimeout(_ => {

      this.setState({ val: this.state.val + 1 })

      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })

      console.log(this.state.val)

    }, 0)

  }

  render() {

    return

{this.state.val}

  }

}

结合上面分析的,钩子函数中的setState无法立马拿到更新后的值,所以前两次都是输出0,当执行到setTimeout里的时候,前面两个state的值已经被更新,由于setState批量更新的策略,this.state.val只对最后一次的生效,为1,而在setTimmout中setState是可以同步拿到更新结果,所以setTimeout中的两次输出2,3,最终结果就为0, 0, 2, 3。

总结 :

setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout中都是同步的。

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

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



对state的执行队列:需要知道state是具有更新队列的,每次调用setState方法都会将当前修改的state放入此队列,react查询到当前setState方法执行完后,进行state数据的合并,合并后再去执行回调,根据合并结果再去执行更新VirtualDom,触发render周期

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

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





setState后,会经历什么

更新:setState→getDerivedStateFromProps→shouldComponentUpdate→render→ componentDidUpdate→componentWillUnmount

forceUpdate会经历什么




浅合并


setState后面有一个setTimeout,哪个先执行?换成Promise,又是哪个先执行






2. 如果我需要上传一个文件,前端展示进度条,用setState会导致合并更新,你有什么好办法?(Promise封装setState、利用setState底层特性,使用异步函数(setTimeout、async)包裹)


Sometimes you might need to know the previous state when updating the state. However, state updates may be asynchronous - this means React may batch multiple setState() calls into a single update.

not use code like this:

this.setState({

//can't rely on the previous value of this.state or this.props when calculating the next value

counter:this.state.counter+this.props.increment

});

Instead, pass setState a function that allows you to access state and props. Using a function with setState guarantees you are working with the most current values of state and props. This means that the above should be rewritten as:

this.setState((state,props)=>({

counter:state.counter+props.increment}));

You can also use a form without props if you need only the state:

this.setState(state=>({counter:state.counter+1}));

Note that you have to wrap the object literal in parentheses, otherwise JavaScript thinks it's a block of code.


App.js

class App extends React.Component{

    constructor(){

        super()

        this.state = {

            isLoggedIn:false

        }

        this.handleClick=this.handleClick.bind(this)

    }

    handerClick(){

        this.setState(prevState=>{

            return{isLoggedIn:!prevState.isLoggedIn}

        })

    }

    render(){

        let buttonText = this.state.isLoggedIn? "Log Out":"Log In"

        return(

           

               

            

        )

    }

}

export default App


setState_第2张图片
setState是异步的,传入回调给setState()避免依赖过期的值,使用的是回调执行时的状态和属性


setState_第3张图片
脏状态


setState_第4张图片
prevState
setState_第5张图片
高阶函数

你可能感兴趣的:(setState)