这篇文章写得很烂,慎入,主要是我不想删

lxf2023-04-09 21:40:01

原文再续,书接上一回!

上一回讲到 ReactDOM.createRoot(...)之后都经历了些什么?,当调用该方法之后返回一个 fiberRoot,通过控制台打印发现有以下输出:

这篇文章写得很烂,慎入,主要是我不想删

具体这些值的可以看上一篇文章,这里就不再讲了。

render

在前面的文章中讲到通过获取 fiberRoot 正是通过 createRoot(...) 函数调用 new ReactDOMRoot(root) 构造函数得到的,在当前文件中我们继续查看源码,发现 ReactDomRoot(...) 构造函数的原型定义了一个方法,当我们的项目中调用的 root.render(<App />) 正是调用了该原型方法。

那么接下来我们看看这个方法是怎么定义的,这里为了方便阅读我省略了一些边界处理的代码:

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
  children: ReactNodeList,
): void {
 
  const root = this._internalRoot;

  const container = root.containerInfo;

  updateContainer(children, root, null, null);
};

在上面的代码中,this._internalRoot 正是我上面图片所展示的内容,root.containerInfo 就是整个项目的根目录,也就是当前的真实 DOM,这个没什么用,在这里,也就是做一些边界处理。

而当前函数中的参数 children 正是我们传进来的 <App/> 组件,最终通过参数的形式调用了 updateContainer(...) 函数。

updateContainer

在开始之前,我们继续看看整体源码是怎么定义的:

// react\packages\react-reconciler\src\ReactFiberReconciler.old.js

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {

 // 获取当前的rootFiber对象
  const current = container.current;

  // 获取程序运行到目前为止的时间,用于进行优先级排序
  const eventTime = requestEventTime();
  // 同步直接返回 `SyncLane` = 1。以后开启并发和异步等返回的值就不一样了,目前只有同步这个模式
  const lane = requestUpdateLane(current);

  if (enableSchedulingProfiler) {
    // 在Performance接口中标记--schedule-render-{当前lane}用于性能分析
    markRenderScheduled(lane);
  }

  // 获取当前节点和子节点的上下文
  const context = getContextForSubtree(parentComponent);

  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  // 创建一个 update 对象
  const update = createUpdate(eventTime, lane);

  // 记录update的载荷信息
  update.payload = {element};

  // 如果有回调信息,保存
  callback = callback === undefined ? null : callback;
  update.callback = callback;
  }
   // 将新建的update入队
  const root = enqueueUpdate(current, update, lane);
  if (root !== null) {
    scheduleUpdateOnFiber(root, current, lane, eventTime);
     
    entangleTransitions(root, current, lane);
  }

  return lane;
}

在函数体的第一行代码中 const current = container.current; 来获取当前可更改的 rootFiber 树。

通过 const eventTime = requestEventTime(); 来获取当前距离 react 应用初始化的时间,而使用 const lane = requestUpdateLane(fiber); 来获取到当前事件对应的 Lane 优先级。

在上面的讲解中通过 requestUpdateLane(...) 函数来获取当前时间的优先级,那么接下来让我们看看它是怎么获取事件的优先级的,让我们来看看该函数是怎么定义的吧,这里省略了一些无关紧要的代码:

export function requestUpdateLane(fiber: Fiber): Lane {
  const mode = fiber.mode;
  if ((mode & ConcurrentMode) === NoMode) {
    // concurrent 模式
    return (SyncLane: Lane);
  } else if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {

    /**
     * 当新的更新任务产生时,workInProgressRootRenderLanes不为空,则表示有任务正在执行
     * 那么则直接返回这个正在执行的任务的lane,那么当前新的任务则会和现有的任务进行一次批量更新
     */  
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

  /**
   * 检查当前事件是否为过渡优先级,例如使用了 Suspense 或者 useTransition
   * 每次调用优先级都会降低
   * 过渡优先级共有16位:当所有位都使用完后,则又从第一位开始赋予事件过渡优先级
   */  
  const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {
    if (currentEventTransitionLane === NoLane) {
      currentEventTransitionLane = claimNextTransitionLane();
    }
    return currentEventTransitionLane;
  }

  /**
   * 返回当前任务优先级
   * updateLane 优先级为 0
   */  
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane;
  }

  /**
   * 返回当前事件的优先级
   * 如果没有事件返回,则返回 DefaultEventPriority
   * 如果有,根据事件类型返回优先级
   * eventLane 优先级为16
   */  
  const eventLane: Lane = (getCurrentEventPriority(): any);

  return eventLane;
}

在上面的代码中通过判断当前是同步模式还是并发模式,如果是并发模式会进行批量更新。

requestCurrentTransition(...) 函数是判断当前更新是否存在 useTransition 或者 Suspence 方法的使用,如果使用了,该函数的返回值不为 null 则条件成立,例如,在我们的项目中使用 useTransition,具体代码如下所示:

import { useState, useTransition } from "react";

const App = () => {
  const [state, setState] = useState(0);
  const [loading, startTransition] = useTransition(2000);
  const foo = () => {
    startTransition(() => {
      setState(1);
    });
  };
  return (
    <div className="moment">
      <div key="1" className="test" onClick={foo}>
        {state}
      </div>
      <div key="2" test="111" className="niu">
        2222
      </div>
    </div>
  );
};

export default App;

我们通过打印 currentEventTransitionLane 的一直点击绑定了点击事件的元素,并观察控制台:

这篇文章写得很烂,慎入,主要是我不想删

说明了每次点击之后该事件的优先级都会降低(数字越高优先级越低),过渡优先级一共有16位,当所有为都使用完之后,则又从第一位开始赋予事件过渡优先级。

在初次渲染的时候,该函数返回的优先级为 事件(eventLane) 的优先级,也是默认的优先级,即16,而当我们进行点击触发 setState 的时候,当前的优先级变为 任务优先级(updateLane),请看下图:

这篇文章写得很烂,慎入,主要是我不想删

上面的图片中的输出的优先级是属于初次渲染的,那么我们通过点击去改变 state 呢?

这篇文章写得很烂,慎入,主要是我不想删

返回了一个1,也就是最高优先级,讲到这里,requestUpdateLane(...) 函数我们也就讲完了,退出当前执行栈,并返回一个16给 updateContainer(...) 函数中的 lane 变量。

接下来从父组件中获取 context,如果存在则设置为 container.pendingContext = context; 否则 container.context = context;

紧接着根据 laneeventTime 创建 update 对象,将 element 挂载到 update.payload.element 上,将 callback 挂载在 update.callback 上。

接下来调用 enqueueUpdate(...) 函数,其实这个函数也没干啥,主要还是调用了 enqueueConcurrentClassUpdate(...) 函数,其实 enqueueConcurrentClassUpdate(...) 函数也没干啥事,也是调用 markUpdateLaneFromFiberToRoot(...) 返回一个新的 fiberRoot,至于 markUpdateLaneFromFiberToRoot(...) 都干了些啥事,这里先不讲,在后面的文章中会讲到,这里只需要知道它会更新 fiberRoot 就可以了。

此时 updateContainer(...) 函数中的 root 变量的值如下所示:

这篇文章写得很烂,慎入,主要是我不想删

当前 root 不为 null,继续调用 scheduleUpdateOnFiber(...),只要涉及到需要改变 fiber 的操作,无论是 首次渲染对比更新,最后都会间接调用scheduleUpdateOnFiberscheduleUpdateOnFiber 函数是输入链路中的必经之路。

在渲染阶段,该函数的主要作用还是调用 ensureRootIsScheduled(...) 函数,该函数主要的作用就是确保 root 在调度,如果任务类型是同步任务,则不需要经过调度,经过scheduleLegacySyncCallback或scheduleSyncCallback包装,如果任务类型是并发任务,则需要经过调度,会通过scheduleCallback回调函数注册调度任务。

接下来我们看看该函数在初次渲染阶段主要做了些啥,为了省略代码,忽略一些不会执行的代码:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {

  // 保存调度单元,scheduler所创建的task对象,与并发模式的调度有关。
  const existingCallbackNode = root.callbackNode;
  // 是否有 lane 饿死,标记为过期。
  markStarvedLanesAsExpired(root, currentTime);

  /**
   * 获取最高 lane 等级的新任务的 lane 值
   * 同时设置全局变量 return_highestLanePriority
   * return_highestLanePriority 对应的是 最高 lane 等级的优先级
   * return_highestLanePriority 可以通过 returnNextLanesPriority 函数返回
   */  
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

// 根据lane,获取优先级
const newCallbackPriority = getHighestPriorityLane(nextLanes);

// 开启一个新的调度
let newCallbackNode;

let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }
     // 保存调度单元 scheduler 所创建的task对象
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );

  // 根据 newCallbackPriority 保存调度最高 lane 等级的优先级
  root.callbackPriority = newCallbackPriority;
  // 保存调度单元 scheduler 所创建的task对象
  root.callbackNode = newCallbackNode;
}

这里的主要功能是查看是否有 lane 饿死并把它标记为过期,紧接着获取下一个 lane 并获取到优先值。

在计算出调度优先级之后,开始让Scheduler调度React的更新任务,调用 scheduleCallback(...) 函数,并传入优先级和 performConcurrentWorkOnRoot.bind(null, root)那么我们看看该函数的主要功能是什么。

performConcurrentWorkOnRoot

该函数内又执行了一些关键函数,主要有以下方面:

  • flushPassiveEffects: 处理副作用,例如 useEffect;
  • ensureRootIsScheduled
  1. 如果任务类型是同步任务,则不需要经过调度,经过scheduleLegacySyncCallback或scheduleSyncCallback包装;
  2. 如果当前的 js 主线程空闲 (没有正在执行的react任务),则执行performSyncWorkOnRoot开始执行同步任务;
  3. 如果任务类型是并发任务,则需要经过调度,会通过scheduleCallback回调函数注册调度任务;;
  • shouldTimeSlice: 表示是否开启时间切片,如果返回 false,则表示使用同步方式渲染,调用 renderRootSync(...),否则调用 renderRootConcurrent(...) 函数,并且最后执行 finishConcurrentRender(...) 函数完成 render 阶段;
  • ensureRootIsScheduled: 检查是否还有任务需要执行;
  • root.callbackNode === originalCallbackNode: 判断开始缓存起来的值是否一样,如果一样说明渲染被阻断,返回一个新的performConcurrentWorkOnRoot函数,等待下一次调用,否则直接返回 null;

到从 performConcurrentWorkOnRoot(...) 函数执行完毕,返回值作为 scheduleCallback(...) 的参数。

scheduleCallback

scheduleCallback 的本质其实是在调用 Scheduler_scheduleCallback 函数,而 Scheduler_scheduleCallback 函数是 unstable_scheduleCallback 函数的别名,其主要代码如下所示:

function unstable_scheduleCallback(priorityLevel, callback, options) {

   // 这个 currentTime 获取的是 performance.now() 时间戳
  var currentTime = getCurrentTime();

   // 任务开始的时间
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

    // 根据调度优先级设置相应的超时时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }


  /**
   * expirationTime 用于标识一个任务具体的过期时间
   * 当前任务在1分钟后过期跟10分钟后过期其实本质上都没有什么区别,因为都还没有过期
   * 但是关键在于10分钟后过期的情况,可以把当前任务稍微放一放,把资源先给其他任务执行
   */  

  var expirationTime = startTime + timeout;

   // 属于 Scheduler 自己的 task
  var newTask = {
    id: taskIdCounter++,
    callback,// 这里的 callback 是 performConcurrentWorkOnRoot 函数
    priorityLevel,// 调度优先级
    startTime, // 任务开始时间
    expirationTime,// 任务过期时间
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // Scheduler调度任务时传入了 delay time,startTime 是大于 currentTime 的,
  // 表示这个任务将会延迟执行
  if (startTime > currentTime) {
   // 当前任务已超时,插入超时队列
    newTask.sortIndex = startTime;

    // timerQueue 是一个二叉堆结构,以最小堆的形式存储 task
    // 向二叉堆中添加节点
    // 待调度的队列
    push(timerQueue, newTask);

     // peek 查看堆的顶点, 也就是优先级最高的`task`
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.

        // 这个任务是最早延迟执行的
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.

          // 取消现有的定时器
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 安排一个定时器
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 任务未超时,插入调度任务队列
    newTask.sortIndex = expirationTime;
    // taskQueue 是一个二叉堆结构,以最小堆的形式存储 task
    // 调度中的队列
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.

     // 符合更新调度执行的标志
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);// 调度任务
    }
  }

  return newTask;
}

该函数的主要作用有如下几个方面:

  • 获取开始时间的 startTime;
  • 根据不同的优先级设置超时时间的 timeout;
  • 设置截止时间;
  • 创建新的 task 对象;
  • 如果任务未过期,把任务推入 timerQueue,检查是否有执行中的 hostTimeout,有的话取消掉,重新开启一个 hostTimeout;
  • 如果任务已过期,把任务推入 taskQueue,开始启动调度;

emmmmmmm,这怎么越写越乱,凑合着看吧,晚点重新整一篇,直接结束吧

这个时候 scheduleUpdateOnFiber(...) 函数调用结束,继续回到 updateContainer(...) 函数,继续执行 markRootEntangled(...),这个函数的主要作用是如果 lane 是过渡车道,则取 root.pendingLanes与 root.updateQueue.shared.lanes 的交集,更新到后者,执行 markRootEntangled

到这里 updateContainer(...) 函数调用结束,最终返回一个值为 16 的优先级。此时效果已经能在浏览器中显示了。

下一篇写一篇长的吧,讲讲 fiber 树是如何构建的,以及初次渲染时的整个流程。