React v16源码之setState触发的更新流程

前言

实际上在之前React v15相关原理的文章中就setState的几个特点有了较为清晰的梳理,虽然在React v16中引入了Fiber架构,但是setState整体的处理逻辑以及使用并没有什么区别,只是底层处理不同而已,为什么还有梳理v16 setState相关的逻辑呢?侧重点不同,本文不会主要去看setState的一些特点的形成原因(在前面的文章中就已经说明了再说就没有意义),主要想通过setState去看视图更新机制。

setState

以简单的实例来梳理整个源码运行流程,实例如下:

    class Hello extends React.Component {
      constructor(props) {
        super(props);
        this.state = { date: Date.now() };
      }
      render() {
        return React.createElement(
          'button',
          {
            onClick: () => {
              debugger
              this.setState({ date: Date.now() })
            }
          },
          this.state.date,
        );
      }
    }

当点击按钮后就会调用setState,其主要逻辑调用对应updater的enqueueSetState方法,和v15版本没有什么区别,区别在于enqueueSetState中的执行逻辑,该函数的主要逻辑如下:

var classComponentUpdater = {
	enqueueSetState: function (inst, payload, callback) {
		...
		var update = createUpdate(expirationTime, suspenseConfig);
       	update.payload = payload;
       	...
       	enqueueUpdate(fiber, update);
       	scheduleWork(fiber, expirationTime);
     }
}

就是创建update对象,调用scheduleWork开始调度作业,是不是跟render挂载过程相似的处理逻辑。在v16版本中update队列和scheduleWork是核心,所有相关的处理都是通过这两个函数来处理的。

setState时scheduleWork的处理逻辑

scheduleWork会调用scheduleUpdateOnFiber函数做相关处理,其中主要逻辑是:

function scheduleUpdateOnFiber(fiber, expirationTime) {
	...
    var priorityLevel = getCurrentPriorityLevel();
    if (expirationTime === Sync) {
       if ( // Check if we're inside unbatchedUpdates
       (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
       (executionContext & (RenderContext | CommitContext)) === NoContext) {
       	 ...
         performSyncWorkOnRoot(root);
       } else {
         ensureRootIsScheduled(root);
         ...
         if (executionContext === NoContext) {
           ...
           flushSyncCallbackQueue();
         }
       }
     } else {
       ensureRootIsScheduled(root);
       ...
     }
     ...
}

setState触发更新是满足expirationTime === Sync,执行ensureRootIsScheduled。该函数的主要执行逻辑代码如下:

function ensureRootIsScheduled() {
	...
	
    if (expirationTime === Sync) {
       // Sync React callbacks are scheduled on a special internal queue
       callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
       callbackNode = scheduleCallback(priorityLevel, performConcurrentWorkOnRoot.bind(null, root), // Compute a task timeout based on the expiration time. This also affects
       // ordering because tasks are processed in timeout order.
       {
         timeout: expirationTimeToMs(expirationTime) - now()
       });
   }
   ...
}

通过Debug知道会执行scheduleSyncCallback函数,该函数的具体逻辑如下:

function scheduleSyncCallback() {
	...
	if (syncQueue == null) {
		syncQueue = [callback];
	    immediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl);
	}
	...
}

从上面逻辑可知会调用Scheduler_scheduleCallback函数,该函数作用是创建task并推入taskQueue中,其中Task的回调函数就是flushSyncCallbackQueueImpl。

当Scheduler_scheduleCallback函数执行完后,代码逻辑会退回到scheduleUpdateOnFiber执行后续逻辑,实际上setState的更新处理逻辑到这里就结束了,那么视图是如何被更新的呢?

通过源码以及调用栈分析,之后会执行flushSyncCallbackQueueImpl函数,该函数就是视图更新的触发,该函数的主要逻辑如下:

 function flushSyncCallbackQueueImpl() {
     if (!isFlushingSyncQueue && syncQueue !== null) {
       // Prevent re-entrancy.
       isFlushingSyncQueue = true;
       var i = 0;
 
       try {
         var _isSync = true;
         var queue = syncQueue;
         // 执行后面的传入函数
         runWithPriority$1(ImmediatePriority, function () {
           for (; i < queue.length; i++) {
             var callback = queue[i];
 
             do {
               callback = callback(_isSync);
             } while (callback !== null);
           }
         });
         syncQueue = null;
       }
      }
   }

从上面的逻辑可知该函数实际上是执行syncQueue中的内容,而syncQueue内容就是之前的performSyncWorkOnRoot,追本溯源还是回到了performSyncWorkOnRoot的具体处理逻辑,这个逻辑实际上在之前的Render执行过程中有相关的梳理(workLoopSync -> performUnitOfWork),实际上核心就是两点:

  • 所有组件渲染更新,涉及到相关生命周期调用等相关逻辑
  • commitRoot:所有组件都更新完成后进入commit阶段,将更新后的视图渲染出来

至此完成一次setState的主要的更新流程。实际上上面整个逻辑中有个非常重要的点就是flushSyncCallbackQueueImpl函数的执行(这个函数总是会被执行的,不清楚来源不影响主要脉络的理解),从上面整个逻辑梳理知道这个函数会处理syncQueue,而整个视图的渲染的入口performSyncWorkOnRoot就被保存在SyncQueue中,但是setState整个过程中并没有触发该函数的执行,那么问题来了flushSyncCallbackQueueImpl函数是在哪里被谁触发调用的呢?

通过源码查找可知flushSyncCallbackQueueImpl直接和间接调用有两处:

  • scheduleSyncCallback:该函数只在ensureRootIsScheduled中被调用
  • flushSyncCallbackQueue

通过一系列的排查,发现flushSyncCallbackQueue的调用涉及到事件机制。setState的触发一般是在事件处理程序中,当然也有其他情况的。这里就以事件处理中触发setState来讲。React中事件是合成事件,所有的事件都是注册在document或挂载点上的,对应的event也是合成对象SyntheticEvent。v16版本中事件处理过程不同了,这里就不展开,主要记住不论怎么变化都是需要原生DOM来触发的。

在React中不同的事件也存在优先级,主要分成三个优先级:

  • DiscreteEvent:离散事件,例如click、blur等
  • UserBlockingEvent:用户阻塞事件,例如drag、scroll等
  • ContinuousEvent:连续事件,例如load等

不同的事件对对应的处理程序是不同的,离散事件对应dispatchDiscreteEvent处理函数,用户阻塞事件对应dispatchUserBlockingUpdate处理函数,连续事件对应dispatchEvent处理函数。click事件就是调用dispatchDiscreteEvent来处理,而dispatchDiscreteEvent函数的处理逻辑如下:

function dispatchDiscreteEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
	...
	discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, container, nativeEvent);
}

discreteUpdates会调用discreteUpdatesImpl函数(即discreteUpdates$1函数),该函数会触发flushSyncCallbackQueue函数的执行,继而触发flushSyncCallbackQueueImpl从而触发视图渲染。

总结

setState触发后的整个逻辑主要脉络如下:

  • 调用class组件对应的updater提供的enqueueState方法
  • 创建update对象,将setState传递的数据挂载到update的指定属性上
  • 对新创建的update对象进行排队,即enqueueUpdate
  • 开始调度作业,即scheduleWork
  • 创建新的task并推入heap堆栈中,实际上整个setState的处理过程到这里就结束了,后续同步代码执行
  • 对于事件处理程序中的代码,会在其所有同步代码执行完毕后,由事件机制触发flushSyncCallbackQueueImpl函数的执行
  • 调用flushSyncCallbackQueueImpl对SyncQueue进行处理,其中会存在一个performSyncWorkOnRoot
  • performSyncWorkOnRoot函数执行,内部会执行workLoopSync方法,同步迭代所有组件的渲染更新
  • 当所有组件都渲染更新后,进入commit阶段,这个阶段会调用runWithPriority$1函数,该函数按照优先级处理相关传入的回调函数commitRootImpl
  • commitRootImpl函数中主要执行三个函数(commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects),在commitMutationEffects函数中就会将更新的DOM更新到浏览器中,更新后的视图就会呈现出来

在上面的脉络中,这里补充下一点,就是workLoopSync中处理所有组件更新,在这个过程中对于class组件其render函数会被调用来获得最新的UI,所以最好保持render函数是一个纯函数。

你可能感兴趣的:(React相关,React,v16,setState,更新流程)