知根知底setState

1,910 阅读5分钟

setState作为react中使用最频繁的一个API,在这里简单分享它的实现机制。
没错本文是一篇讲源码的文章,但尽量避免做代码的搬运工,根据setState的使用场景进行解析,源码基于react v16.4.3-alpha.0

一,预备知识

1,fiber

网上有很多讲解fiber的文章大多在描述fiber的算法。实际上fiber包含数据结构和算法,按照v16之前的版本理解,fiber在源码中表示虚拟DOM的一个节点

2,react的事件系统

对react有一定了解的同学肯定知道react封装了一套自己的事件系统,<div onClick={handleClick}></div>并不是像vue一样调用addEventListener绑定事件到对应的节点上,而是通过事件委托的方式绑定到document上了
接下来我们简单来看实现过程:

  // 获取任意一个通过react渲染得到的DOM节点
  const someElement = document.getElementById('#someId')
  
  // 打印节点元素
  console.dir(someElement)
  
  // 任何一个通过react渲染得到的DOM节点都会有`__reactEventHandlers****`这个属性
  console.dir(someElement.__reactEventHandlers****)
  
  // __reactEventHandlers中可以找到在JSX中为这个标签添加的事件属性
  const onClick = someElement.__reactEventHandlers****.onClick
  

有了上面的知识我们看一下react事件系统的简易过程

  • 点击一个按钮触发document上的click事件
  • 获得事件对象event
  • 通过event.target可以知道是点击的那个按钮
  • 拿到按钮上面的__reactEventHandlers
  • 然后就有了onClick
// 伪代码
documnet.addEventListener('click', function(event){
    const target = event.target
    const onClick = traget.__reactEventHandlers*****.onClick
    
    // isBatchingUpdates全局变量后面会具体讲解到
    var previousIsBatchingUpdates = isBatchingUpdates;
    isBatchingUpdates = true;
    
    try {
        // 执行事件回调
        return onClick(event);
    } finally {
        isBatchingUpdates = previousIsBatchingUpdates;
        performSyncWork()
    }
})

这里只是简要描述,实际实现要复杂很多

二,走一遍源码

1,setState实现

在这里可以看源码

Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

2,this.updater

this.updater 是在哪个地方进行赋值的我们暂时不用关心,只需要知道他被赋值为classComponentUpdater

3,classComponentUpdater

在这里可以看源码 我们只需关心生成了update,插入到update队列,然后调用scheduleWork

// 伪代码
const classComponentUpdater = {
  ...
  enqueueSetState(inst, payload, callback) {
    const update = createUpdate(expirationTime);
    // setState(payload, callback);
    update.payload = payload;
    update.callback = callback;
    
    // 插入到update队列
    enqueueUpdate(fiber, update);
    
    scheduleWork(fiber, expirationTime);
  },
  ...

4,scheduleWork

在这里可以看源 这一步我们只需关心下面的这一段逻辑

// isWorking、isCommitting是全部变量,在后面我们会具体分析到
if (
    !isWorking ||
    isCommitting ||
    nextRoot !== root
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }

5,requestWork

function requestWork(root, expirationTime) {
  // 将根节点添加到调度任务中
  addRootToSchedule(root, expirationTime)
  
  // isRendering是全局变量,在后面我们会具体分析到
  if (isRendering) {
    return;
  }
  
  // isBatchingUpdates、isUnbatchingUpdates是全局变量
  // 在第一节了解react事件时有对他们进行重新赋值
  if (isBatchingUpdates) {
    if (isUnbatchingUpdates) {
      ....
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }
  
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

好了,要了解setState的过程,追踪到这五步就可以了,下面会结合具体场景来对这整个过程具体分析

三,使用场景

1,交互事件

handleClick(){
    this.setState({
        name: '吴彦祖'
    })
    console.log(this.state.name) // >> 狗蛋
    this.setState({
        age: '18'
    })
    console.log(this.state.age) // >> 40
}

第一节中了解到在执行事件回调handleClick前isBatchingUpdates = true,滚动到看第二节的源码过程,最终在第五步requestWork中会执行

function requestWork(){
    ...
    if (isBatchingUpdates) {
        return
    }
    ...
}

第一个setState也就到此为止被return,接着执行第二个setState同样到这一步为止。
现在我们能知道什么呢?

在交互事件中的setState每次执行只是创建了一个新的update,然后添加到enqueueUpdate,setState并没有直接触发react的update

再回头看第一节中react的事件过程,当handleClick执行后会立马调用performWork开始react的update过程

理一下整个过程,交互事件中的因为isBatchingUpdates = true会先收集所有的update到enqueueUpdate中,交互事件回调执行完后再调用performWork一次更新所有的state

现在来思考一个问题,setState是异步的?

从源码可以看到这整个过程对浏览器来说都是同步的,一步一步顺序执行;对于开发者来说,执行setState后因为要进行批处理操作,而延后了react的更新

2,setTimeout、setInterval、Promise中

在1中我们知道因为isBatchingUpdates = true的原因执行setState后无法直接拿到新的state,如果我们可以避免isBatchingUpdates的问题结果又会怎样

handleClick(){
   setTimeout(() => {
       this.setState({
           name: '吴彦祖'
       })
       console.log(this.state.name) // >> 吴彦祖
       this.setState({
           age: '18'
       })
       console.log(this.state.age) // >> 18
   })
}

通过setTimeout执行setState,也就没有react的事件系统什么事了, isBatchingUpdates的默认为false,看第二节第五步,每次setState都会执行performSyncWork触发react的update,所以每次调用setState紧接着我们就能拿到最新的state

通过在setTimeout中执行setState我们达到了setState是同步的效果,当然通过setInterval、Promise也能达到同样的效果。

3,componentWillUpdate (render前生命周期)

先补充一个知识点

在第二节源码中可以注意到三个全局变量:isRenderingisWorkingisCommitting

v16中react更新有两个阶段reconciler和commit阶段

  • isRendering:开始react更新就为true
  • isWorking:进入reconciler阶段就为true、进入commit阶段就为true
  • isCommitting:进入commit阶段就为true

render前生命周期属于reconciler阶段:isRendering = trueisWorking = true

触发第二节第五步:

function requestWork(){
    ...
    if (isRendering) {
        return
    }
    ...
}

render前生命周期不会触发新的更新,只是将新的update添加到enqueueUpdate尾部,在当前更新任务中处理

4,componentDidUpdate (render后生命周期)

render后生命周期属于commit阶段:isRendering = trueisWorking = trueisCommitting = true
同样会触发第二节第五步:

function requestWork(){
    addRootToSchedule(root, expirationTime)
    if (isRendering) {
        return
    }
    ...
}

render后生命周期不会立即触发新的更新,当然也不会在本次更新任务中处理,这里我们注意有一个addRootToSchedule(root, expirationTime),将新的更新作为下一个更新任务

例:

修改name触发componentDidUpdate()componentDidUpdate修改age

过程: 修改name开始react的update过程完成reconciler和commit阶段,因为任务中还有一个修改age的任务,再次开始react的update过程完成reconciler和commit阶段

注意:在componentDidUpdate使用setState可能会造成死循环

结尾

react本人用的不是很多,结合官方文档暂时只能想到上述四种场景。为仅讲解setState文中刻意省略了fiber相关的过程,后面有机会会有fiber相关的分享。有什么建议欢迎在下面留言交流。