理解React如何实现批量状态更新

1,469 阅读2分钟

这是我参与更文挑战的第15天,活动详情查看: 更文挑战

背景

我们知道React的setState方法并不是同步执行的。 在React的生命周期中发生的多次setState的变更会进行合并,最终减少推送给浏览器的DOM变更次数,从而提升前端性能。那么这部分到底是怎么实现的呢?

  1. 事件处理函数自带batchedUpdates
  2. setTimeout中没有batchedUpdates
  3. 主动batchedUpdates

第1种:

批量更新,会打印 0 0 0,然后按钮文本显示为1。每次 setState 虽然都会经过 enqueueUpdate (创建update 并加入队列)-> scheduleWork(寻找对应的 FiberRoot 节点)-> requestWork (把 FiberRoot 加入到调度队列),可惜上下文变量 isBatchingUpdates 在外部某个地方被标记为了 true ,因此本次 setState 一路走来,尚未到达接下来的 performSyncWork 或者 scheduleCakkbackWithExpirationTime 就开始一路 return 出栈。

isBatchingUpdates 变量在早前的调用栈中(我们为 onClick 绑定的事件处理函数会被 react 包裹多层),被标记为了 true ,然后 fn(a, b) 内部经过了3次 setState 系列操作,然后 finally 中 isBatchingUpdates 恢复为之前的 false,此时执行同步更新工作 performSyncWork 。

第2种:

在 add 中使用 setTimeout 将 this.countNumber 包裹了一层 setTimeout(() => { this.countNumber()}, 0) ,同样要调用 add 也是先经过 interactiveUpdates1上下文,也会执行setTimeout,然后fn(a,b)就执行完了,因为最终是浏览器来调用setTimeout的回调然后执行里面的this.countNumber,而对于interactiveUpdates1 上下文,也会执行 setTimeout ,然后 fn(a, b) 就执行完了,因为最终是浏览器来调用 setTimeout 的回调 然后执行里面的 this.countNumber ,而对于 interactiveUpdates1 来说继续把自己的 performSyncWork 执行完,就算结束了。显然不管 performSyncWork 做了什么同步更新,我们的 ​setState 目前为止都还没得到执行。然后等到 ​setTimeout 的回调函数等到空闲被执行的时候,才会执行 ​setState ,此时没有了批量更新之上下文,所以每个 ​setState 都会单独执行一遍 ​requestWork 中的 ​performSyncWork 直到渲染结束,且不会被打断,3次 ​setState 就会整个更新渲染 3 遍(这样性能不好,所以一般不会这样写 react)。

什么叫不会被打断的同步更新渲染?看一下 demo 中的输出,每次都同步打印出了最新的 button dom 的 ​innerText 。

第3种:

已经可以猜到,无非就是因为使用 ​setTimeout 而“错过了”第一次的批量更新上下文,那等到 ​setTimeout 的回调执行的时候,专门再创建一个批量更新上下文即可。

function App() {
   const [a, setA] = useState(0);
   
   function add() {
       setTimeout(() => {
           console.log('run 4');
           setA(a + 3);
           console.log('run 5');
           setA(a + 8);
           console.log('run 6');
       });
       console.log('run 1');
       setA(a + 1);
       console.log('run 2');
       setA(a + 1);
       console.log('run 3');
   }
   console.log('trigger');
   return (
       <Fragment>
           <div onClick={add}>{a}</div>
       </Fragment>
   )
}