【学习笔记】React18源码学习(一) 设计理念与Scheduler

415 阅读8分钟

是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

【学习笔记】React18源码学习(二)Reconciler

1. React哲学

我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。

制约快速响应的两个瓶颈 CPU 和 IO

  • CPU

浏览器60Hz,所以是1000ms/60=16.6ms 浏览器刷新一次,在这个刷新期间浏览器会执行

graph LR
JS脚本执行 --> 样式布局 --> 样式绘制

如果js脚本执行超过16ms 就会阻塞后面的布局,就会造成页面的卡顿。

react解决方案:将同步更新变为异步可中断的更新

  • IO

react 将人机交互的研究成果整合到真实的UI中

2. 新老架构

老的React架构 react15:

graph LR
Reconcile协调器--> Render渲染器

老的react架构为同步的更新,递归执行,数据保存在调用栈中被称为Stack Reconciler

新的Fiber架构架构

graph LR
Scheduler调度器 --> Reconciler协调器 --> Render渲染器

新的架构调度器负责决定更新的优先级,协调器负责创建Fiber,和生成FiberList,渲染器负责将fiberList渲染为真实的DOM,所以被称为Fiber Reconciler

React为实现异步可中断的更新,使用Fiber(纤程)。 Fiber架构有两个特性

  1. 更新可以中断并有空余的时间继续
  2. 高优先级的更新可以中断低优先级的更新\

Fiber的三层含义

  1. 作为架构Fiber Reconciler
  2. 作为静态的数据结构 虚拟DOM
  3. 作为动态的工作单元 需要更新的状态需要执行的副作用
// FiberNode
 // Instance
  this.tag = tag; // fiber类型 比如div就是Host Component
  this.key = key; // 键 列表的标记
  this.elementType = null;
  this.type = null; // 类型 对于functionComponent 是 function,对于Class Component 是 构造函数,对于Host Component 是 TagName
  this.stateNode = null; // 对于Host Component 是 DOM对应的真实节点

  // Fiber
  this.return = null; // 将节点连接在一起
  this.child = null; // 子节点
  this.sibling = null; // 兄弟节点
  this.index = 0; // 多个同级节点插入DOM的位置

  this.ref = null; // 同Ref
  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; // current fiber指向work in progress fiber;working in progress fiber指向current fiber

双缓存与Fiber

概念:在内存中构建两个Fiber树,一棵叫CurrentFiber,WorkInProgreesFiber,解决卡顿

Demo

function App() {
    const [num,add] = useState(0)
    return (
        <p onClick={() => add(num + 1)}>{num}</p>
    )
 }
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <App />
);
  1. ReactDOM.createRoot 会创建整个应用的根节点FiberRootNode
  2. root.render 创建应用的子节点RootFiber
graph TD

A[FiberRootNode] -- current --> B[RootFiber]
  1. 进入首屏渲染的逻辑
  2. 创建一棵WorkInProgressFiber树,第一个节点为RootFiber,由于currentFiber已存在所以会用alternate属性连接,方便两个节点
  3. 将剩余节点用深度优先遍历创建整棵fiber树
  4. 创建完成后将 current 指向 WorkInProgressFiber
  5. 更新逻辑
  6. 点击P标签,触发更新,用jsx 与Current Fiber使用Diff算法做对比(首屏渲染与触发更新的区别),生成WorkInProgressFiber,构建完成后将current指向

3. Scheduler调度器

脚踏N条船的时间管理大师

跑起来代码,打开chrome,打开控制台,打开Performance,点击录制,点暂停,看到时间轴里蓝色的标签位置也就是DOMContentLoaded Event,放大再放大。

image.png

可以看到调度器阶段依次执行的四个方法,接下来就逐个分析

  1. performWorkUntilDeadline

debug一下会发现call stack里面从index.js开始执行

import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <App />
);

在 React 18 中,ReactDOM.createRoot 已经被废弃,取而代之的是 ReactDOM.createRootContainer 方法。不过它们的原理都是一样的。

ReactDOMRoot.prototype.render()

root.render 会调用 ReactDOMRoot.prototype.render()

function (children) {
  // 这里的root 就是上面真实的DOM的root节点
  var root = this._internalRoot;
  updateContainer(children, root, null, null);
};

updateContainer

// 删掉了一些 devtools 和 性能分析 相关不用考虑的代码
function updateContainer(element, container, parentComponent, callback) {
  const current = container.current; // FiberRootNoe
  const eventTime = requestEventTime(); //当前事件执行的时间
  // 优先级为32
  // 查看ReactFiberLane.js 为 DefaultLane
  const lane = requestUpdateLane(current); 
  const update = createUpdate(eventTime, lane);
  update.payload = {element};
/** 
  👆两步执行结果
  update = {
    eventTime: eventTime,
    lane: lane,
    tag: UpdateState,
    payload: element,
    callback: null,
    next: null
  };
**/
  // 入队更新
  const root = enqueueUpdate(current, update, lane);
  // 生成FiberRootNode 且 current 指向了 RootFiber
  if (root !== null) {
    // 调度更新
    scheduleUpdateOnFiber(root, current, lane, eventTime);
    // 处理在更新期间可能出现的暂停和恢复操作
    entangleTransitions(root, current, lane);
  }
 return lane;
}

调用 enqueueUpdate 函数将 update 添加到当前 Fiber 节点的更新队列中,并传递 current 表示当前 Fiber 节点。如果当前 Fiber 节点没有更新队列,那么会创建一个新的更新队列。

之后,返回的 root 指针指向的是当前 Fiber 节点的更新队列中的最后一个更新对象。

这个更新对象包含了最新的更新内容。同时,这个更新对象还会记录一些元数据,例如哪些更新 lane(优先级)需要更新、哪些更新需要被合并等。

通过entangleTransitions这个函数会将 current Fiber 节点的暂停操作和 root恢复操作添加到一个双向链表中,并将它们绑定在一起。这个链表称为FiberRoot entanglements链表。在 scheduleCallbackForRoot 函数中,React Fiber 架构会遍历这个链表,并执行“暂停”操作和“恢复”操作。

entangleTransitions 函数被调用时,它会检查当前的 Fiber 节点是否已经被暂停,如果是,则会将当前节点的“暂停”操作添加到 root 的“恢复”操作上。如果当前节点没有被暂停,那么不需要执行任何操作。

scheduleUpdateOnFiber

export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
   // 标记root的更新
   markRootUpdated(root, lane, eventTime);
   ensureRootIsScheduled(root, eventTime);
   if (
      lane === SyncLane &&
      executionContext === NoContext &&
      (fiber.mode & ConcurrentMode) === NoMode &&
      // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
      !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
    ) {
      // Flush the synchronous work now, unless we're already working or inside
      // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
      // scheduleCallbackForFiber to preserve the ability to schedule a callback
      // without immediately flushing it. We only do this for user-initiated
      // updates, to preserve historical behavior of legacy mode.
      resetRenderTimer();
      flushSyncCallbacksOnlyInLegacyMode();
    }
}
  • root:表示该组件所在的 Fiber 树的根节点(即 FiberRoot 对象)。
  • fiber:表示要更新的组件对应的 Fiber 节点。
  • lane:表示更新的优先级。
  • eventTime:表示更新的时间戳。

scheduleUpdateOnFiber 函数中,首先会调用 markUpdateLaneFromFiberToRoot 函数,该函数的作用是将更新的优先级 lane 标记在从当前 fiber 节点到根节点 root 的所有节点上。

接下来,会调用 ensureRootIsScheduled为了确保组件对应的 Fiber 树能够被及时地进行更新,需要注意的是ensureRootIsScheduled并不会立即执行更新操作,而是将组件对应的 Fiber 树加入到调度器中,等待后续调度器进行调度,以便在适当的时间执行更新操作

ensureRootIsScheduled

newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  • schedulerPriorityLevel 是一个枚举值,表示回调函数的优先级。

React 中定义了几个优先级常量,包括 ImmediatePriorityUserBlockingPriorityNormalPriorityLowPriorityIdlePriority 等。

不同的优先级对应不同的操作,例如 UserBlockingPriority 用于处理用户交互事件,LowPriority 用于执行一些低优先级的操作。

这里的优先级是NormalPriority

  • callback

performConcurrentWorkOnRoot.bind(null, root),第一个参数 null 表示回调函数中的 this 值为全局对象,scheduleCallback 函数返回一个 callbackNode 对象,用于标识和取消回调函数,返回的 callbackNode 赋值给了 newCallbackNode 变量,表示新创建的回调函数的标识。可以通过这个标识,随时取消回调函数的执行。

flushWork

function flushWork(hasTimeRemaining: boolean, initialTime: number) {
  // We'll need a host callback the next time work is scheduled.
  const previousPriorityLevel = currentPriorityLevel;
  try {
      // 执行workLoop 这里就是开始render阶段了
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

4.承上启下调度器与协调器的过渡

WorkLoop

/** 
@param hasTimeRemaining 剩余时间
@param initialTime 初始时间
**/
function workLoop(hasTimeRemaining: boolean, initialTime: number) {
  let currentTime = initialTime;
  // 检查不再延迟的任务并将其添加到队列中。
  advanceTimers(currentTime);
  // 从taskQueue中取出一个任务
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 当前任务过期
      break;
    }
    // 当前 callBack 是 performConcurrentWorkOnRoot(root, didTimeout) {
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // BreakPoint
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // 如果返回了一个continuation,则立即让位给主线程 而不管当前时间片中还剩多少时间。
        currentTask.callback = continuationCallback;
        advanceTimers(currentTime);
        return true;
      } else {
         
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

重点在这里

const continuationCallback = callback(didUserCallbackTimeout);

这里执行的callBack 方法来源于 ensureRootIsScheduled的最后一行

newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));

所以执行的是这个performConcurrentWorkOnRoot。

performConcurrentWorkOnRoot

function performConcurrentWorkOnRoot(root: FiberRoot, didTimeout: boolean) {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
        throw '当前不应处于React工作流程内';
    }
    // 开始执行具体工作前,保证上一次的useEffct都执行了
    // 同时要注意useEffect执行时触发的更新优先级是否大于当前更新的优先级
    const didFlushPassiveEffects = flushPassiveEffects(
            root.pendingPassiveEffects
    );
    const curCallbackNode = root.callbackNode;
    if (didFlushPassiveEffects) {
            if (root.callbackNode !== curCallbackNode) {
                    // 调度了更高优更新,这个更新已经被取消了
                    return null;
            }
    }
    const lanes = getNextLanes(root);
    if (lanes === NoLanes) {
            return null;
    }
    // 本次更新是否是并发更新?
    // TODO 饥饿问题也会影响shouldTimeSlice
    const shouldTimeSlice = !didTimeout;
    const exitStatus = renderRoot(root, lanes, shouldTimeSlice);
    ensureRootIsScheduled(root);
    if (exitStatus === RootIncomplete) {
        if (root.callbackNode !== curCallbackNode) {
            // 调度了更高优更新,这个更新已经被取消了
            return null;
        }
        return performConcurrentWorkOnRoot.bind(null, root);
    }
    // 各种边界的 if else。。。省略
    // 现在我们有了一个一致的树形结构。下一步是要提交它
    // 或者如果有什么被暂停了,就等待一段时间后再提交它。
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    finishConcurrentRender(root, exitStatus, lanes);
    // 首先,使用 `ensureRootIsScheduled` 函数来确保当前的根节点被调度。
    ensureRootIsScheduled(root, now());
    if (root.callbackNode === originalCallbackNode) {
    // The task node scheduled for this root is the same one that's
    // currently executed. Need to return a continuation.
    if (
      workInProgressSuspendedReason === SuspendedOnData &&
      workInProgressRoot === root
    ) {
      // 工作循环当前处于暂停状态并等待数据解析。
      // 在这种情况下,需要取消当前任务,以便在数据解析完成后重新调度
      root.callbackPriority = NoLane;
      root.callbackNode = null;
      return null;
    }
    // 如果没有特殊情况需要处理,则返回一个函数 
    // 绑定了 `root` 参数,它用于执行并发模式下的工作循环。
    // 它会递归遍历 fiber 节点树,执行节点上的任务,直到所有的任务都执行完成。
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}