【Vue/React】React中的setState异步、同步与进阶

setState的定义及运行机制

在 React 日常的使用中,一个很重要的点就是,不要直接去修改 state。例如:this.state.count = 1是无法触发 React 去更新视图的。因为React的机制规定,一个state的更新,首先需要调用 setState 方法。

this.setState({
    count: 1
})

这样便能触发重新渲染。稍有经验的开发者会知道,setState 方法其实是 “异步” 的。即立马执行之后,是无法直接获取到最新的 state 的,需要经过 React 对 state 的所有改变进行合并处理之后,才会去计算新的虚拟dom,再根据最新的虚拟dom去重新渲染真实dom。

class App extends Component {
    state = {
        count: 0
    }

    componentDidMount(){
        this.setState({count: this.state.count + 1})
        console.log(this.state.count) // 0
    }

    render(){
        ...
    }
}
1. 合成事件中的setState

定义:
首先得了解一下什么是合成事件,react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新前的val --> 0
  }
  render() {
    return (
      <div onClick={this.increment}>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

合成事件中的setState写法比较常见,点击事件里去改变 this.state.val 的状态值,在 increment 事件中打个断点可以看到调用栈,这里我贴一张自己画的流程图:
在这里插入图片描述
dispatchInteractiveEventcallCallBack 为止,都是对合成事件的处理和执行,从 setStaterequestWork 是调用 this.setState 的逻辑,这边主要看下 requestWork 这个函数(从 dispatchEventrequestWork 的调用栈是属于 interactiveUpdates$1try 代码块,下文会提到)

function requestWork(root, expirationTime) {
  addRootToSchedule(root, expirationTime);

  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpiration(expirationTime);
  }
}

requestWork 中有三个if分支,三个分支中有两个方法 performWorkOnRootperformSyncWork ,就是我们默认的 update函数,但是在合成事件中,走的是第二个if分支,第二个分支中有两个标识 isBatchingUpdatesisUnbatchingUpdates 两个初始值都为 false ,但是在 interactiveUpdates$1 中会把 isBatchingUpdates 设为 true ,下面就是 interactiveUpdates$1 的代码:

function interactiveUpdates$1(fn, a, b) {
  if (isBatchingInteractiveUpdates) {
    return fn(a, b);
  }
  // If there are any pending interactive updates, synchronously flush them.
  // This needs to happen before we read any handlers, because the effect of
  // the previous event may influence which handlers are called during
  // this event.
  if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPendingInteractiveExpirationTime, false, null);
    lowestPendingInteractiveExpirationTime = NoWork;
  }
  var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingInteractiveUpdates = true;
  isBatchingUpdates = true;  // 把requestWork中的isBatchingUpdates标识改为true
  try {
    return fn(a, b);
  } finally {
    isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

在这个方法中把 isBatchingUpdates 设为了 true ,导致在 requestWork 方法中, isBatchingUpdates 为 true ,但是 isUnbatchingUpdates 是 false ,而被直接return了。

那return完的逻辑回到哪里呢,最终正是回到了 interactiveUpdates 这个方法,仔细看一眼,这个方法里面有个try finally语法,前端同学这个其实是用的比较少的,简单的说就是会先执行 try 代码块中的语句,然后再执行 finally 中的代码,而 fn(a, b) 是在try代码块中,刚才说到在 requestWork 中被return掉的也就是这个fn(上文提到的 从 dispatchEventrequestWork 的一整个调用栈)。

所以当你在 increment 中调用 setState 之后去console.log的时候,是属于 try 代码块中的执行,但是由于是合成事件,try代码块执行完state并没有更新,所以你输入的结果是更新前的 state 值,这就导致了所谓的"异步",但是当你的try代码块执行完的时候(也就是你的 increment 合成事件),这个时候会去执行 finally 里的代码,在 finally 中执行了 performSyncWork 方法,这个时候才会去更新你的 state 并且渲染到UI上。

2. 原生事件中的setState
class App 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>
    )
  }
}

————————————
| demo 执行工具 在线测试 |
————————————

原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件。
在这里插入图片描述
原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发click事件,到 requestWork ,在 requestWork 里由于 expirationTime === Sync 的原因,直接走了 performSyncWork 去更新,并不像合成事件或钩子函数中被return,所以当你在原生事件中setState后,能同步拿到更新后的state值。

3. callback中的setState

那怎么才能获取到修改后的state呢? React为我们提供了一个回调去实现

...
this.setState({count: this.state.count + 1}, ()=>{
    console.log(this.state.count) // 1
})
...

回调里的 state 便是最新的了,原因是 该回调的执行时机在于state合并处理之后 。如果我们这样去做:

...
    this.setState({count: this.state.count + 1})
    this.setState({count: this.state.count + 1})
    console.log('count:',this.state.count)  // 0
...

实际最终的 count 会等于 1,原因是执行时得到的 this.state.count = 0。那怎么实现结果为 2 呢?

...
this.setState(prevState => {count: prevState.count + 1});
this.setState(prevState => {count: prevState.count + 1});
...

setState()实际上可以接受一个函数作为参数,函数的首个参数就是上一次的state。

以上介绍了setState的三种使用方式,下面我们来看看它们的执行时机是怎样的:

...
    this.setState({ count: this.state.count + 1 });
    console.log("console: " + this.state.count); // 0
    this.setState({ count: this.state.count + 1 }, () => {
      console.log("console from callback: " + this.state.count); // 2
    });
    this.setState(prevState => {
      console.log("console from func: " + prevState.count); // 1
      return {
        count: prevState.count + 1
      };
    }, ()=>{
      console.log('last console: '+ this.state.count)
    });
...

执行结果:

console: 0 
console from func: 1 
console from callback: 2
last console: 2 

————————————
| demo 执行工具 在线测试 |
————————————
React 其实会维护着一个 state 的更新队列,每次调用 setState 都会先把当前修改的 state 推进这个队列,在最后,React 会对这个队列进行合并处理,然后去执行回调。根据最终的合并结果再去走下面的流程(更新虚拟dom,触发渲染)。

4. 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,但是不管是哪个场景下,基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。

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

5. 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 (
      <div onClick={this.batchUpdates}>
        {`Counter is ${this.state.val}`} // 1
      </div>
    )
  }
}

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

function createUpdateQueue(baseState) {
  var queue = {
    expirationTime: NoWork,
    baseState: baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
  };
  return queue;
}

function appendUpdateToQueue(queue, update, expirationTime) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > expirationTime) {
    // The incoming update has the earliest expiration of any update in the
    // queue. Update the queue's expiration time.
    queue.expirationTime = expirationTime;
  }
}
总结
  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  2. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
参考

你真的理解setState吗?
浅出深入setState(上)
浅出深入setState(下)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值