简单解读react的setState

274 阅读2分钟

本文将以一道面试题“react中setState是同步的还是异步?”进行解读。

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

setState表现为异步的场景如下:

1.合成事件中的setState

react自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。

class SetStatus extends Component {
    state = { val: 0 }
    increment = () => {
        debugger
        this.setState({val: this.state.val +1})
        console.log('---------'. val) // 0
    }
    render() {
        return (
            <div onClick={this.increment}>
                {`Counter is: ${this.state.val}`}
            </div>
        )
    }
}

只有当increment函数执行完以后,才会更新state,执行performSyncWorkOnRoot

2.生命周期函数中的setState

class StudySetState extends Component {
    state = {val: 0}

    componentDidMount() {
        debugger
        this.setState({val: this.state.val + 1})
        console.log('-----', this.state.val) // 0
    }

    render() {
        return (
           <div>
              {`Counter is ${this.state.val}`}
           </div>
        )
    }
}

原理:其实还是和合成事件一样,当 componentDidmount 执行的时候,react内部并没有更新,执行完componentDidmount 后才去 commitUpdateQueue 更新。这就导致你在 componentDidmountsetState 完去console.log拿的结果还是更新前的值。
源码真相:

if ( finishedWork.mode & ProfileMode) {
     try {
           startLayoutEffectTimer();
           instance.componentDidMount(); // 执行完componentDidMount()
     } finally {
           recordLayoutEffectDuration(finishedWork);
     }
  } else {
   instance.componentDidMount();
}
var updateQueue = finishedWork.updateQueue;
 if (updateQueue !== null) {
     {
       if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
          if (instance.props !== finishedWork.memoizedProps) {
             error('Expected %s props to match memoized props before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
     }
          if (instance.state !== finishedWork.memoizedState) {
             error('Expected %s state to match memoized state before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.state`. ' + 'Please file an issue.', getComponentNameFromFiber(finishedWork) || 'instance');
      }
   }
} // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.
 commitUpdateQueue(finishedWork, updateQueue, instance); // 更新props和state
 }

setState表现为同步的场景如下:

1.原生事件的setState

class StudySetState extends Component {

    state = { val: 0 }
    changeValue = () => {
        this.setState({val: this.state.val + 1})
        console.log('-----', this.state.val) // 1
    }

    componentDidMount() {
        document.body.addEventListener('click', this.changeValue, false)
    }
    render() {
        return (
            <div>
                {`Counter is: ${this.state.val}`}
            </div>
        )
    }
 }   

原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件。

原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发click事件,到 requestWork ,在requestWork里由于 expirationTime === Sync 的原因,直接走了 performSyncWork 去更新,并不像合成事件或钩子函数中被return,所以当你在原生事件中setState后,能同步拿到更新后的state值。

2.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 (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

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

不同react版本下setState的表现

在react18之前

  • 在组件生命周期或React合成事件中,setState是异步
  • 在setTimeout或者原生dom事件以及Promise这类不是由React进行发起的事件中,setState是同步

在react18之后

  • 在React18之后,默认所有的操作都被放到了批处理中(异步处理)
  • 如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作
class StudySetState extends Component {

    state = { val: 0}

    handleClick = () => {
        flushSync(() => {
            this.setState({val: 2})
        })
        console.log('-------', this.state.val) // 2
    }

    render() {
        return (
            <button onClick={this.handleClick}>
                {`Counter is: ${this.state.val}`}
            </button>
        )
    }
}