react18源码学习(三),render篇

542 阅读9分钟

react渲染分为render阶段 和commit阶段

本篇我们主要了解下render阶段,react主要做了什么,并发和同步模式下虽然函数调用链不同,但核心的还是那几个函数,我们以核心函数做讲解。

什么是render阶段

render阶段主要是深度优先遍历我们的dom结构,生成一颗fiber树,是采用递归的形式进行的。从页面的根节点(一个页面只有一个根节点),rootFiber开始,向下查找它的第一个子节点,生成对应的fiber节点,并挂载到这颗rootFiber的child属性上,并在这个阶段进行diff比较,为fiber节点打上标记flag(更新、新增、删除等),以此类推,直到末级节点,再向上归溯,这个过程会生成节点对应的dom实例,如果是新增直接生成dom节点到fiber节点的stateNode上,节点发生更新则会生成updateQueue,结构如下:updateQueue: (2) ['children', '7']; 并将dom实例,挂载到虚拟dom树上,直到向上归溯到根节点 rootFiber。这就生成了一棵完整的fiber树

对于fiber结构不太了解的可以查看另一篇文章 juejin.cn/editor/draf…

render阶段,执行了很多函数,其中workLoopSync就是执行递归的入口函数

render阶段“递归”的对象是什么? 是带状态的fiber节点

递的主要入口函数为beginWork,归的主要入口函数completeWork,这两个函数具体做了什么?形成了什么产物?

render阶段的函数调用情况

我们创建一个调试项目,npm start后打开控制台

我们的页面结构如下:

image.png 这里点击p标签更新num,将会改变code中的数字,以此观察react在页面初始和state更新时,源码的执行情况。

启动项目,进入浏览器,打开控制台 录制performance,可以看到整体函数的调用情况

我们将函数调用分为两种情况,一个是初始化,一个是更新 红色部分就是render阶段执行的函数,绿色部分为commit阶段执行的函数,打开call tree我们可以看到更直观的函数调用栈

初始化

image.png image.png

更新

image.png image.png

看到这么多函数调用,我脑瓜子嗡嗡的,这还折叠了一些,看来我离资深又进了一步(头发越来越稀少了)

我们可以看到更新和初始化,两者的函数调用栈有所不同,这是因为react18引入了并发 concurrent并发的概念,这里初始化时,默认为并发模式,render的主要入口为performConcurrentWorkOnRoot,下面的执行栈也与更新有所不同,而更新时为同步模式,render的主要入口为performSyncWorkOnRoot 虽然主要入口有所不同,但执行render递归的核心函数还是一样的(renderRootSync、workLoopSync、performUnitWork、beginWork、completeWork、commitRoot等)

为啥会有这个区别呢,待研究。。。

并发模式和同步模式有什么区别呢?

接下来我们结合performanc看看完成render流程的主要函数

render阶段的主要函数讲解

workInProgress

这是react的一个全局变量,用于存储当前处理的fiber节点

调度入口ensureRootIsScheduled

这个函数是render的入口函数,内部会判断当前render任务的优先级,如果有更高优先级的任务,当前任务会被打断,其次会判断当前更新是同步还是并发,同步通过syncQueue中插入performSyncWorkOnRoot,然后再通过##### flushSyncCallbacks将syncQueue中的任务依次执行,并发下则通过Scheduler(Scheduler模块)来调度performConcurrentWorkOnRoot任务

function ensureRootIsScheduled(root, currentTime) {
  var existingCallbackNode = root.callbackNode; // 当前render阶段的任务

  markStarvedLanesAsExpired(root, currentTime); // 获取root节点上的未执行的任务,并遍历出过期任务,并标记它们

  // 获取需要执行下一个赛道
  var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
   // 赛道为空,代表不需要执行当前阶段的任务,通过schduler删除当前任务
  if (nextLanes === NoLanes) {
    // Special case: There's nothing to work on.
    if (existingCallbackNode !== null) {
      cancelCallback$1(existingCallbackNode);
    }

    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  } 

   // 比较当前赛道与其他赛道,得到最高优先级的
  var newCallbackPriority = getHighestPriorityLane(nextLanes); 
  
  // 判断是同步任务还是并发任务 
  if (includesSyncLane(newCallbackPriority)) {
    // 这里同步任务放在单独的队列中执行,不通过schduler直接调度
    if (root.tag === LegacyRoot) {
      if ( ReactCurrentActQueue$2.isBatchingLegacy !== null) {
        ReactCurrentActQueue$2.didScheduleLegacyUpdate = true;
      }

      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }

    {
      // 将队列放入微任务中执行
      if ( ReactCurrentActQueue$2.current !== null) {
        ReactCurrentActQueue$2.current.push(flushSyncCallbacks);
      } else {
        scheduleMicrotask(function () {
          if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
            
            flushSyncCallbacks();
          }
        });
      }
    }

    newCallbackNode = null;
  } else { // 并发任务
  // 会比较当前赛道的优先级
    var schedulerPriorityLevel;

    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediatePriority;
        break;

      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingPriority;
        break;

      case DefaultEventPriority:
        schedulerPriorityLevel = NormalPriority;
        break;

      case IdleEventPriority:
        schedulerPriorityLevel = IdlePriority;
        break;

      default:
        schedulerPriorityLevel = NormalPriority;
        break;
    }
    // 通过Scheduler调度任务
    newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
} 
scheduleSyncCallback

我们可以看到scheduleSyncCallback只是将任务推入栈中,并没有其他操作

function scheduleSyncCallback(callback) {
  // Push this callback into an internal queue. We'll flush these either in
  // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
  if (syncQueue === null) {
    syncQueue = [callback];
  } else {
    // Push onto existing queue. Don't need to schedule a callback because
    // we already scheduled one when we created the queue.
    syncQueue.push(callback);
  }
}
function scheduleLegacySyncCallback(callback) {
  includesLegacySyncCallbacks = true; // 这是标志的什么?
  scheduleSyncCallback(callback);
}

接下来我们讲下scheduleMicrotask和flushSyncCallbacks这两个函数的作用

scheduleMicrotask
// 判断浏览器promise是否支持
var localPromise = typeof Promise === 'function' ? Promise : undefined;
// 判断queueMicrotask是否支持,不支持则通过promise创建微任务
var scheduleMicrotask = typeof queueMicrotask === 'function' ? 
queueMicrotask : typeof localPromise !== 'undefined' ? function (callback) {
  return localPromise.resolve(null).then(callback).catch(handleErrorInNextTick);
} : scheduleTimeout; // TODO: Determine the best fallback here.

function handleErrorInNextTick(error) {
  setTimeout(function () {
    throw error;
  });
} 

queueMicroTask是window对象上的一个方法,developer.mozilla.org/zh-CN/docs/… 它通过使用立即 resolve 的 promise 创建一个微任务(microtask),如果无法创建 promise,则回落(fallback)到使用setTimeout()

这里scheduleMicrotask的目的是通过queueMicroTask或promise创建一个微任务,并将flushSyncCallbacks做为微任务的第一个回调,其目的是让浏览器优先执行flushSyncCallbacks

flushSyncCallbacks

image.png

image.png 我们看到flushSyncCallbacks实际上是将ensureRootIsScheduled推入syncQueue队列中的函数一一执行,这里指performSyncWorkOnRoot,

performSyncWorkOnRoot

主要目的是执行renderRootSync

// 同步渲染
function performSyncWorkOnRoot(root) {
// 记录更新行为
  {
    syncNestedUpdateFlag();
  }
   // 判断render环境
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    throw new Error('Should not already be working.');
  }
    // 执行所有effect tag任务
  flushPassiveEffects();
  // 获取下一个赛道
  var lanes = getNextLanes(root, NoLanes);
// 不存在赛道,则重新执行ensureRootIsScheduled,确保所有更新被执行
  if (!includesSyncLane(lanes)) {
    // There's no remaining sync work left.
    ensureRootIsScheduled(root, now());
    return null;
  }
   // 重点,开始执行render,并保存渲染状态
  var exitStatus = renderRootSync(root, lanes);
  // 渲染出错的措施
  if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
    var originallyAttemptedLanes = lanes;
    var errorRetryLanes = getLanesToRetrySynchronouslyOnError(root, originallyAttemptedLanes);

    if (errorRetryLanes !== NoLanes) {
      lanes = errorRetryLanes;
      // 在这里会再次调用renderRootSync
      exitStatus = recoverFromConcurrentError(root, originallyAttemptedLanes, errorRetryLanes);
    }
  }
   // 再次出错,重新调用入口函数,并抛出错误跳出当前执行
  if (exitStatus === RootFatalErrored) {
    var fatalError = workInProgressRootFatalError;
    prepareFreshStack(root, NoLanes);
    markRootSuspended$1(root, lanes);
    ensureRootIsScheduled(root, now());
    throw fatalError;
  }

  if (exitStatus === RootDidNotComplete) {
    // 渲染未完成,需要退出当前渲染,
    markRootSuspended$1(root, lanes);
    ensureRootIsScheduled(root, now());
    return null;
  } 

  // render结束,将render阶段生成的最新fiber树赋值到进入commitRoot
  var finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
  // 在退出前需要确保所有未执行的调度任务被执行,

  ensureRootIsScheduled(root, now());
  return null;
}
performConcurrentWorkOnRoot

大部分处理同performSyncWorkOnRoot,这里会判断shouldTimeSlice 是否需要切片来决定执行renderRootConcurrentrenderRootSync

image.png

renderRootSync

主要目的是执行workLoopSync image.png

renderRootConcurrent

主要目的是执行workLoopConCurrent image.png

workLoopSync

同步执行performUnitOfWork 生成fiber节点 image.png

workLoopConCurrent

可以看到与workLoopSync有一点不同,当判断shouldYield为false,将暂停当前处理,并发处理

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
performUnitOfWork

结合workLoopSync或workLoopConCurrent,执行beginWork,beginWork返回当前fiber节点的子节点(child) 当返回了子节点,则改变外部变量workInProgress的值为child workLoopSync继续循环 当子节点为null,也就是当前节点不存在子节点时,执行completeUnitOfWork

function performUnitOfWork(unitOfWork) {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  var current = unitOfWork.alternate;
  setCurrentFiber(unitOfWork);
  var next; // 用于储存当前节点的子节点

  if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    // 执行beginwork,并返回当前节点的子节点
    next = beginWork$1(current, unitOfWork, renderLanes$1); 
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork$1(current, unitOfWork, renderLanes$1);
  }

  resetCurrentFiber();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // 当节点不存在子节点(末级节点),则从当前节点开始执行completeWork
    completeUnitOfWork(unitOfWork);
  } else {
  // 存在子节点,则将workInProgress赋值为子节点,使得workLoopSync/workLoopConCurrent继续循环
    workInProgress = next;
  }

  ReactCurrentOwner$2.current = null;
}

beginwork

目的是创建当前节点的第一个属性值为child的fiber节点,首先会判断当前fiber节点类型做不同操作,进入不同更新逻辑。 image.png 这里在执行到函数方法时,如果页面上已经存在fiber节点,会进入updateFunctionComponent函数

updateFunctionComponent

这里会判断页面上是否已经存在fiber树(current)并且当前节点没有被更新,决定新建还是复制对应的fiber节点,存在时复制页面中的fiber到正在执行的fiber,否则就执行reconcileChildren image.png

bailoutOnAlreadyFinishedWork

image.png

cloneChildFibers

其中cloneChildFibers就是克隆对应的fiber节点,其中执行了createWorkInProgress image.png 我们可以看到这里clone的时候,是将当前节点下的子节点都进行clone,子节点存在兄弟节点时,也会循环将其clone,是通过creatWorkInProgress创建

createWorkInProgress

image.png 在这个方法其目的是创建fiber节点,当当前节点存在alternate属性,则将alternate对应的fiber节点属性复制,如果不存在则新建fiber节点 image.png

var createFiber = function (tag, pendingProps, key, mode) {
  return new FiberNode(tag, pendingProps, key, mode);
};
function FiberNode(tag, pendingProps, key, mode) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null; // Fiber

  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  this.refCleanup = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode; // Effects

  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  this.alternate = null;

  {
   // 这段是为了解决V8下的性能下降的问题
    this.actualDuration = Number.NaN;
    this.actualStartTime = Number.NaN;
    this.selfBaseDuration = Number.NaN;
    this.treeBaseDuration = Number.NaN; 
    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }

  {
    // This isn't directly used but is handy for debugging internals:
    this._debugSource = null;
    this._debugOwner = null;
    this._debugNeedsRemount = false;
    this._debugHookTypes = null;

    if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
      Object.preventExtensions(this);
    }
  }
}


reconcileChildren

其余节点类型,主要是通过执行reconcileChildren方法,在这个方法中判断current属性是否为null,为null时创建,并且不标记child节点的effectTag属性,。不为null则更新当前节点的child 值,并标记effectTag属性, image.png

创建调用mountChildFibers方法。更新调用reconcileChildFibers,注mountChildFibers和reconcileChildFibers其实都是调用同一个方法 createChildReconciler方法,只是参数一个为true,一个为false,

image.png

createChildReconciler中也存在一个函数名为reconcileChildFibers的函数,此函数中 会判断要处理的子节点类型。执行不同的创建fiber节点操作。并为节点打上effectTag标签,方便commit阶段做相应的增删改操作

function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));

        case REACT_PORTAL_TYPE:
          return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes));

        case REACT_LAZY_TYPE:
          var payload = newChild._payload;
          var init = newChild._init; // TODO: This function is supposed to be non-recursive.

          return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes);
      }

      if (isArray(newChild)) {
        return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
      }

    if (typeof newChild === 'string' && newChild !== '' || typeof newChild === 'number') {
      return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes));
    }

    {
      if (typeof newChild === 'function') {
        warnOnFunctionType(returnFiber);
      }
    } 
  }

这里判断了子节点类型做不同操作 当子节点是单一的react元素时,最终也是执行了上面提到的createWorkInProgress创建fiber节点

image.png shouldTrackSideEffects为mountChildFibers或reconcileChildFibers传入的布尔值,这里为true时,并且找不到alternate属性,也就是只有元素新增时,才会打上新增的标记

当子节点是数组时

image.png 可以看到在createChildReconciler函数中,当子节点为数组时,执行了reconcileChildrenArray方法,处理的对象为子节点数组 pendingProps 这里以处理的节点为header对应时,数据结构如下 image.png

深度优先遍历:需要注意的是beginWork操作的节点下存在多个平行的子节点,也会只生成的第一个子节点的fiber。子节点的兄弟元素,则存放在子fiber节点的sibling属性中,这是一个链表结构 以App为例。当workInProgress为header时,header存在img,p,a三个子元素数组 这里当reconcileChildrenArray处理这个子元素数组时,只会生成第一个子元素,也就是img的fiber,此时img对应的fiber节点为 image.png 其中sibling则存储img的下一个兄弟元素 p的fiber节点,p中的sibling则对应 p的下一个兄弟元素 a的fiber节点,以此类推。这个sibling会在completeUnitOfWork中发挥作用

completeUnitOfWork

入参为当前操作的fiber节点,也就是workInProgress

目的是为了改变workInProgress,将workLoopSync继续或者停止

function completeUnitOfWork(unitOfWork) {
  var completedWork = unitOfWork;
  do {
    var current = completedWork.alternate;
    var returnFiber = completedWork.return; // 获取当前节点的父节点
    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentFiber(completedWork);
      var next = void 0;

      if ( (completedWork.mode & ProfileMode) === NoMode) {
        next = completeWork(current, completedWork, renderLanes$1);
      } else {
        startProfilerTimer(completedWork);
        next = completeWork(current, completedWork, renderLanes$1); 

        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }
      resetCurrentFiber();
      if (next !== null) {
        workInProgress = next;
        return;
      }
    } else {
      // 。。。。中间有部分代码省略
      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its subtree flags.
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      } else {
        // We've unwound all the way to the root.
        workInProgressRootExitStatus = RootDidNotComplete;
        workInProgress = null;
        return;
      }
    }
    var siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    completedWork = returnFiber; // 指向父节点
    workInProgress = completedWork;
  } while (completedWork !== null); // 向上归到根节点
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

设立变量completedWork,以此判断是否递归完毕 循环执行completedWork函数

completedWork

主要做了什么?

completedWork根据不同节点类型,会做不同操作。其主要目的是生成/更新dom实例或组件实例 image.png 这里以hostComponent,也就是普通dom组件, 这里分为两种情况 这里判断是否存在current,和当前的stateNode,如果存在,代表是更新,否则就会重新生成dom实例 更新时,执行updateHostComponent$1更新fiber节点


  updateHostComponent$1 = function (current, workInProgress, type, newProps) {
    // 如果是存在alternate,代表是更新
    // newProps 对应workInProgress.pengdingProps===workInProgress.memoizedProps,
    // 用于存储react元素对应的children,clasaName等信息
    var oldProps = current.memoizedProps; // 当前页面中节点对应的props对象
    if (oldProps === newProps) {
      // 如果相等,不做更新
      return;
    } 
    // 获取最新fiber节点的dom实例
    var instance = workInProgress.stateNode;
    // 
    var currentHostContext = getHostContext(); 
    
    // 对比获取有哪些更新,结果为数组
    var updatePayload = prepareUpdate(instance, type, oldProps, newProps, currentHostContext); 
    
    // 将更新队列放入当前workInProgress中
    workInProgress.updateQueue = updatePayload;

    if (updatePayload) {
      markUpdate(workInProgress); // 更新fiber节点的flags标记,更新为4
    }
  };

image.png

当新建时

通过createInstance创建当前节点的dom实例,createInstance中会调用createElement方法创建dom节点,并将创建的实例添加到虚拟dom树中 image.png 如果当前节点不是dom元素,比如是我们创建的组件,那是不会生成stateNode的; 如我们在页面中增加一个自定义组件 image.png image.png completedWork完成后,我们打印看下这个Test节点对应的fiber节点为 image.png

  1. completedWork执行完后,检测当前fiber节点是否存在兄弟节点,也就是sibling属性是否为null,如果存在,则改变workInProgress为兄弟节点,并return,循环结束,completeUnitOfWork执行结束,调用栈回到workLoopSync,workLoopSync中的循环继续,进入beginWork

3.如果不存在兄弟节点,则将completedWork设置为当前节点的父节点(return属性),递归继续,直到递归到根fiber节点,当根节点不存在retrun时,也就是不存在父节点时,workInProgress为null,workLoopSync循环结束,render阶段结束

总结

react在render阶段,主要是生成以页面根节点开始的fiber树,一个页面上只有一颗,从根元素开始向下递归遍历页面上的每一个元素,生成对应的fiber节点,子元素通过child连接(只存放第一个子元素,其余子元素放在child的sibling属性上),父节点通过 return属性链接,兄弟元素通过sibling属性访问,元素对应的dom实例则放在stateNode上,注意节点是函数不会生成stateNode。

graph TD
root --> App --> div --> header --> img &  p & a 

从根fiber节点出发,交替执行beginWork、completeWork,beginWork从app向下一层一层生成第一个子节点,并返回,直到末级,再逆向向上执行completeWork生成当前fiber节点的dom实例,如生成完毕后查看当前节点是否有兄弟节点,不存在,则回到父级节点的completeWork。有则执行第一个兄弟节点的beginWork,completeWork循环,直到所有兄弟节点循环完成,再向上追溯,直到页生成页面对应的完整fiber树,render阶段结束。

流程拆解如下

1.从根fiber节点出发,创建它的第一个子fiber节点 App,挂载到根fiber节点的child上,并返回

2.返回为App的fiber对象,再进入这个子fiber节点 APP,创建它的子fiber节点 div 并返回

3.进入div节点,创建并返回header节点

4.进入header节点,此时存在多个子节点 img p a ,创建第一个子节点img,其它节点存放在img节点的sibling属性上,返回img节点

5.进入img,执行completeWork,创建img的dom实例,并挂载到img节点的stateNode,挂载完成,不存在子节点,此时返回null,

6.查看img节点是否存在sibling,存在p,img阶段的completeWork结束

7.对p节点执行第5、6步,以此类推,直到a节点执行完成,回到header层(不存在兄弟元素),向上执行completeWork,直到root,此时生成完整fiber树

image.png