时间切片的实现和调度(原创2.6万字)

本人系一个惯用Vue的菜鸡,恰巧周末和大佬扯蛋,峰回路转谈到了fiber,被大佬疯狂鄙视...前端

大佬还和我吐槽了如今的忘了环境node

  1. 百度是不可信的,百度到的东西出来广告其余都是出自同一个做者(大部分状况确实这样)
  2. 不少水文都是以 copy 的形式产生的,你看到的文章说不定已通过时好几个版本了(大部分状况确实这样)

因而本菜开始了 React Fiber 相关的读源码过程。为何看 Fiber?由于 Vue 没有,Vue3 也没有,可是却被吹的很神奇。react

本菜于编写时间于:2020/05/25,参考的当日源码版本 v16.13.1git

Fiber的出现是为了解决什么问题? <略过一下>

首先必需要知道为何会出现 Fibergithub

旧版本React同步更新:当React决定要加载或者更新组件树时,会作不少事,好比调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树。数组

举个栗子:更新一个组件须要1毫秒,若是要更新1000个组件,那就会耗时1秒,在这1秒的更新过程当中,主线程都在专心运行更新操做。浏览器

而浏览器每间隔必定的时间从新绘制一下当前页面。通常来讲这个频率是每秒60次。也就是说每16毫秒( 1 / 60 ≈ 0.0167 )浏览器会有一个周期性地重绘行为,这每16毫秒咱们称为一帧。这一帧的时间里面浏览器作些什么事情呢:安全

  1. 执行JS。
  2. 计算Style。
  3. 构建布局模型(Layout)。
  4. 绘制图层样式(Paint)。
  5. 组合计算渲染呈现结果(Composite)。

若是这六个步骤中,任意一个步骤所占用的时间过长,总时间超过 16ms 了以后,用户也许就能看到卡顿。而上述栗子中组件同步更新耗时 1秒,意味着差很少用户卡顿了 1秒钟!!!(差很少 - -!)bash

由于JavaScript单线程的特色,每一个同步任务不能耗时太长,否则就会让程序不会对其余输入做出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。数据结构

什么是 Fiber <略过一下>

解决同步更新的方案之一就是时间切片:把更新过程碎片化,把一个耗时长的任务分红不少小片。执行非阻塞渲染,基于优先级应用更新以及在后台预渲染内容。

Fiber 就是由 performUnitOfWork(ps:后文详细讲述) 方法操控的 工做单元,做为一种数据结构,用于表明某些worker,换句话说,就是一个work单元,经过Fiber的架构,提供了一种跟踪,调度,暂停和停止工做的便捷方式。

Fiber的建立和使用过程:

  1. 来自render方法返回的每一个React元素的数据被合并到fiber node树中
  2. React为每一个React元素建立了一个fiber node
  3. 与React元素不一样,每次渲染过程,不会再从新建立fiber
  4. 随后的更新中,React重用fiber节点,并使用来自相应React元素的数据来更新必要的属性。
  5. 同时React 会维护一个 workInProgressTree 用于计算更新(双缓冲),能够认为是一颗表示当前工做进度的树。还有一颗表示已渲染界面的旧树,React就是一边和旧树比对,一边构建WIP树的。 alternate 指向旧树的同等节点。

PS:上文说的 workInProgress 属于 beginWork 流程了,若是要写下来差很少篇幅还会增长一倍,这就不详细说明了...(主要是本人懒又菜...)

Fiber的体系结构分为两个主要阶段:reconciliation(协调)/render 和 commit

React 的 Reconciliation 阶段 <略过一下>

Reconciliation 阶段在 Fiber重构后 和旧版本思路差异不大, 只不过不会再递归去比对、并且不会立刻提交变动。

涉及生命钩子

  • shouldComponentUpdate
  • componentWillMount(废弃)
  • componentWillReceiveProps(废弃)
  • componentWillUpdate(废弃)
  • static getDerivedStateFromProps

reconciliation 特性:

  • 能够打断,在协调阶段若是时间片用完,React就会选择让出控制权。由于协调阶段执行的工做不会致使任何用户可见的变动,因此在这个阶段让出控制权不会有什么问题。
  • 由于协调阶段可能被中断、恢复,甚至重作,React 协调阶段的生命周期钩子可能会被调用屡次!, 例如 componentWillMount 可能会被调用两次。
  • 所以协调阶段的生命周期钩子不能包含反作用,因此,该钩子就被废弃了

完成 reconciliation 过程。这里用的是 深度优先搜索(DFS),先处理子节点,再处理兄弟节点,直到循环完成。

React 的 Commit 阶段 <略过一下>

涉及生命钩子

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount(废弃)
  • getSnapshotBeforeUpdate

rendercommit:不能暂停,会一直更新界面直到完成

Fiber 如何处理优先级?

对于UI来讲须要考虑如下问题:

并非全部的state更新都须要当即显示出来,好比:

  • 屏幕以外的部分的更新并非全部的更新优先级都是同样的
  • 用户输入的响应优先级要比经过请求填充内容的响应优先级更高
  • 理想状况下,对于某些高优先级的操做,应该是能够打断低优先级的操做执行的

因此,React 定义了一系列事件优先级

下面是优先级时间的源码

[源码文件](github.com/facebook/re…

var maxSigned31BitInt = 1073741823;

  // Times out immediately
  var IMMEDIATE_PRIORITY_TIMEOUT = -1;
  // Eventually times out
  var USER_BLOCKING_PRIORITY = 250;
  var NORMAL_PRIORITY_TIMEOUT = 5000;
  var LOW_PRIORITY_TIMEOUT = 10000;
  // Never times out
  var IDLE_PRIORITY = maxSigned31BitInt;
复制代码

当有更新任务来的时候,不会立刻去作 Diff 操做,而是先把当前的更新送入一个 Update Queue 中,而后交给 Scheduler 去处理,Scheduler 会根据当前主线程的使用状况去处理此次 Update。

无论执行的过程怎样拆分、以什么顺序执行,Fiber 都会保证状态的一致性和视图的一致性。

如何保证相同在必定时间内触发的优先级同样的任务到期时间相同? React 经过 ceiling 方法来实现的。。。本菜没使用过 | 语法...

下面是处理到期时间的 ceiling 源码

[源码文件](github.com/facebook/re…

function ceiling(num, precision) {
  return (((num / precision) | 0) + 1) * precision;
}
复制代码

那么为何须要保证时间一致性?请看下文。

Fiber 如何调度?

首先要找到调度入口地址 scheduleUpdateOnFiber

每个root都有一个惟一的调度任务,若是已经存在,咱们要确保到期时间与下一级别任务的相同(因此用上文提到的 ceiling 方法来控制到期时间)

源码文件

export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTime, ) {
  checkForNestedUpdates();
  warnAboutRenderPhaseUpdatesInDEV(fiber);

  // 调用markUpdateTimeFromFiberToRoot,更新 fiber 节点的 expirationTime
  // ps 此时的fiber树只有一个root fiber。
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }

  // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.
  // 还只是TODO
  // computeExpirationForFiber还会读取优先级。
  // 将优先级做为参数传递给该函数和该函数。
  const 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
    ) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      // 在根上注册待处理的交互,以免丢失跟踪的交互数据。
      schedulePendingInteractions(root, expirationTime);

      // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);
      if (executionContext === NoContext) {
        // 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.
        // 推入调度任务队列
        flushSyncCallbackQueue();
      }
    }
  } else {
    // Schedule a discrete update but only if it's not Sync.
    if (
      (executionContext & DiscreteEventContext) !== NoContext &&
      // Only updates at user-blocking priority or greater are considered
      // discrete, even inside a discrete event.
      (priorityLevel === UserBlockingPriority ||
        priorityLevel === ImmediatePriority)
    ) {
      // This is the result of a discrete event. Track the lowest priority
      // discrete update per root so we can flush them early, if needed.
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
      } else {
        const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
        if (
          lastDiscreteTime === undefined ||
          lastDiscreteTime > expirationTime
        ) {
          rootsWithPendingDiscreteUpdates.set(root, expirationTime);
        }
      }
    }
    // Schedule other updates after in case the callback is sync.
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }
}
复制代码

上面源码主要作了如下几件事

  1. 调用 markUpdateTimeFromFiberToRoot 更新 Fiber 节点的 expirationTime
  2. ensureRootIsScheduled(更新重点)
  3. schedulePendingInteractions 实际上会调用 scheduleInteractions
  • scheduleInteractions 会利用FiberRoot的 pendingInteractionMap 属性和不一样的 expirationTime,获取每次schedule所需的update任务的集合,记录它们的数量,并检测这些任务是否会出错。

更新的重点在于 scheduleUpdateOnFiber 每一次更新都会调用 function ensureRootIsScheduled(root: FiberRoot)

下面是 ensureRootIsScheduled 的源码

源码文件

function ensureRootIsScheduled(root: FiberRoot) {
  const lastExpiredTime = root.lastExpiredTime;
  if (lastExpiredTime !== NoWork) {
    // Special case: Expired work should flush synchronously.
    root.callbackExpirationTime = Sync;
    root.callbackPriority_old = ImmediatePriority;
    root.callbackNode = scheduleSyncCallback(
      performSyncWorkOnRoot.bind(null, root),
    );
    return;
  }

  const expirationTime = getNextRootExpirationTimeToWorkOn(root);
  const existingCallbackNode = root.callbackNode;
  if (expirationTime === NoWork) {
    // There's nothing to work on.
    if (existingCallbackNode !== null) {
      root.callbackNode = null;
      root.callbackExpirationTime = NoWork;
      root.callbackPriority_old = NoPriority;
    }
    return;
  }

  // TODO: If this is an update, we already read the current time. Pass the
  // time as an argument.
  const currentTime = requestCurrentTimeForUpdate();
  const priorityLevel = inferPriorityFromExpirationTime(
    currentTime,
    expirationTime,
  );

  // If there's an existing render task, confirm it has the correct priority and
  // expiration time. Otherwise, we'll cancel it and schedule a new one.
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority_old;
    const existingCallbackExpirationTime = root.callbackExpirationTime;
    if (
      // Callback must have the exact same expiration time.
      existingCallbackExpirationTime === expirationTime &&
      // Callback must have greater or equal priority.
      existingCallbackPriority >= priorityLevel
    ) {
      // Existing callback is sufficient.
      return;
    }
    // Need to schedule a new task.
    // TODO: Instead of scheduling a new task, we should be able to change the
    // priority of the existing one.
    cancelCallback(existingCallbackNode);
  }

  root.callbackExpirationTime = expirationTime;
  root.callbackPriority_old = priorityLevel;

  let callbackNode;
  if (expirationTime === Sync) {
    // Sync React callbacks are scheduled on a special internal queue
    callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
    callbackNode = scheduleCallback(
      priorityLevel,
      performConcurrentWorkOnRoot.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()},
    );
  }

  root.callbackNode = callbackNode;
}
复制代码

上面源码 ensureRootIsScheduled 主要是根据同步/异步状态作不一样的 push 功能。

同步调度 function scheduleSyncCallback(callback: SchedulerCallback)

  • 若是队列不为空就推入同步队列(syncQueue.push(callback)
  • 若是为空就当即推入 任务调度队列(Scheduler_scheduleCallback)
  • 会将 performSyncWorkOnRoot 做为 SchedulerCallback

下面是 scheduleSyncCallback 源码内容

源码文件

export function scheduleSyncCallback(callback: SchedulerCallback) {
  // 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];
    // Flush the queue in the next tick, at the earliest.
    immediateQueueCallbackNode = Scheduler_scheduleCallback(
      Scheduler_ImmediatePriority,
      flushSyncCallbackQueueImpl,
    );
  } 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);
  }
  return fakeCallbackNode;
}

复制代码

异步调度,异步的任务调度很简单,直接将异步任务推入调度队列(Scheduler_scheduleCallback),会将 performConcurrentWorkOnRoot 做为 SchedulerCallback

export function scheduleCallback( reactPriorityLevel: ReactPriorityLevel, callback: SchedulerCallback, options: SchedulerCallbackOptions | void | null, ) {
  const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
  return Scheduler_scheduleCallback(priorityLevel, callback, options);
}
复制代码

无论同步调度仍是异步调度,都会通过 Scheduler_scheduleCallback 也就是调度的核心方法 function unstable_scheduleCallback(priorityLevel, callback, options),它们会有各自的 SchedulerCallback

小提示:因为下面不少代码中会使用 peek,先插一段 peek 实现,其实就是返回数组中的第一个 或者 null

peek 相关源码文件

export function peek(heap: Heap): Node | null {
    const first = heap[0];
    return first === undefined ? null : first;
  }
复制代码

下面是 Scheduler_scheduleCallback 相关源码

[源码文件](github.com/facebook/re…

// 将一个任务推入任务调度队列
function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();

  var startTime;
  var timeout;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    } 
    timeout =
      typeof options.timeout === 'number'
        ? options.timeout
        : timeoutForPriorityLevel(priorityLevel);
  } else {
    // 针对不一样的优先级算出不一样的过时时间
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }
  
   // 定义新的过时时间
  var expirationTime = startTime + timeout;

  // 定义一个新的任务
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;

    // 将超时的任务推入超时队列
    push(timerQueue, newTask);
    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;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;

    // 将新的任务推入任务队列
    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;
        (flushWork);
    }
  }

  return newTask;
}
复制代码

小提示: markTaskStart 主要起到记录的功能,对应的是 markTaskCompleted

源码文件

export function markTaskStart( task: { id: number, priorityLevel: PriorityLevel, ... }, ms: number, ) {
  if (enableProfiling) {
    profilingState[QUEUE_SIZE]++;

    if (eventLog !== null) {
      // performance.now returns a float, representing milliseconds. When the
      // event is logged, it's coerced to an int. Convert to microseconds to
      // maintain extra degrees of precision.
      logEvent([TaskStartEvent, ms * 1000, task.id, task.priorityLevel]);
    }
  }
}

export function markTaskCompleted( task: { id: number, priorityLevel: PriorityLevel, ... }, ms: number, ) {
  if (enableProfiling) {
    profilingState[PRIORITY] = NoPriority;
    profilingState[CURRENT_TASK_ID] = 0;
    profilingState[QUEUE_SIZE]--;

    if (eventLog !== null) {
      logEvent([TaskCompleteEvent, ms * 1000, task.id]);
    }
  }
}
复制代码

unstable_scheduleCallback 主要作了几件事

  • 经过 options.delayoptions.timeout 加上 timeoutForPriorityLevel() 来得到 newTaskexpirationTime
  • 若是任务已过时
    • 将超时任务推入超时队列
    • 若是全部任务都延迟时,并且该任务是最先的任务,会调用 cancelHostTimeout
    • 调用 requestHostTimeout
  • 将新任务推入任务队列

源码文件

补上 cancelHostTimeout 源码

cancelHostTimeout = function() {
    clearTimeout(_timeoutID);
  };
复制代码

再补上 requestHostTimeout 源码

requestHostTimeout = function(cb, ms) {
    _timeoutID = setTimeout(cb, ms);
  };
复制代码

而后 requestHostTimeoutcb 也就是 handleTimeout 是啥呢?

function handleTimeout(currentTime) {
    isHostTimeoutScheduled = false;
    advanceTimers(currentTime);

    if (!isHostCallbackScheduled) {
      if (peek(taskQueue) !== null) {
        isHostCallbackScheduled = true;
        requestHostCallback(flushWork);
      } else {
        const firstTimer = peek(timerQueue);
        if (firstTimer !== null) {
          requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
        }
      }
    }
  }
复制代码

上面这个方法很重要,它主要作了下面几件事

  1. 调用 advanceTimers 检查再也不延迟的任务,并将其添加到队列中。

下面是 advanceTimers 源码

function advanceTimers(currentTime) {
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}
复制代码
  1. 调用 requestHostCallback 经过 MessageChannel 的异步方法来开启任务调度 performWorkUntilDeadline

requestHostCallback 这个方法特别重要

源码文件

// 经过onmessage 调用 performWorkUntilDeadline 方法
channel.port1.onmessage = performWorkUntilDeadline;

// postMessage
requestHostCallback = function(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    port.postMessage(null);
  }
};
复制代码

而后是同文件下的 performWorkUntilDeadline,调用了 scheduledHostCallback, 也就是以前传入的 flushWork

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // Yield after `yieldInterval` ms, regardless of where we are in the vsync
    // cycle. This means there's always time remaining at the beginning of
    // the message event.
    deadline = currentTime + yieldInterval;
    const hasTimeRemaining = true;
    try {
      const hasMoreWork = scheduledHostCallback(
        hasTimeRemaining,
        currentTime,
      );
      if (!hasMoreWork) {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        port.postMessage(null);
      }
    } catch (error) {
      // If a scheduler task throws, exit the current browser task so the
      // error can be observed.
      port.postMessage(null);
      throw error;
    }
  } else {
    isMessageLoopRunning = false;
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};
复制代码

flushWork 主要的做用是调用 workLoop 去循环执行全部的任务

源码文件

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // No catch in prod codepath.
      return workLoop(hasTimeRemaining, initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}
复制代码

workLoopflushWork 在一个文件中,做用是从调度任务队列中取出优先级最高的任务,而后去执行。

还记得上文讲的 SchedulerCallback 吗?

  • 对于同步任务执行的是 performSyncWorkOnRoot
  • 对于异步的任务执行的是 performConcurrentWorkOnRoot
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    const callback = currentTask.callback;
    if (callback !== null) {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      markTaskRun(currentTask, currentTime);
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        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;
  }
}
复制代码

最终都会经过 performUnitOfWork 操做。

这个方法只不过异步的方法是能够打断的,咱们每次调用都要查看是否超时。

源码文件

function performUnitOfWork(unitOfWork: Fiber): void {
  // 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.
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, renderExpirationTime);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderExpirationTime);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}
复制代码

上面的 startProfilerTimerstopProfilerTimerIfRunningAndRecordDelta 其实就是记录 fiber 的工做时长。

源码文件

function startProfilerTimer(fiber: Fiber): void {
  if (!enableProfilerTimer) {
    return;
  }

  profilerStartTime = now();

  if (((fiber.actualStartTime: any): number) < 0) {
    fiber.actualStartTime = now();
  }
}

function stopProfilerTimerIfRunningAndRecordDelta( fiber: Fiber, overrideBaseTime: boolean, ): void {
  if (!enableProfilerTimer) {
    return;
  }

  if (profilerStartTime >= 0) {
    const elapsedTime = now() - profilerStartTime;
    fiber.actualDuration += elapsedTime;
    if (overrideBaseTime) {
      fiber.selfBaseDuration = elapsedTime;
    }
    profilerStartTime = -1;
  }
}
复制代码

最后,就到了 beginWork 流程了 - -。里面有什么呢? workInProgress 还有一大堆的 switch case

想看 beginWork 源码的能够自行尝试 beginWork相关源码文件

总结

最后是总结部分,该不应写这个想了好久,每一个读者在不一样时间不一样心境下看源码的感悟应该是不同的(固然本身回顾的时候也是读者)。每次看应该都有每一个时期的总结。

可是若是不写总结,这篇解析又感受枯燥无味,且没有结果。因此简单略过一下(确定是原创啦,别的地方没有的)

  1. fiber其实就是一个节点,是链表的遍历形式
  2. fiber 经过优先级计算 expirationTime 获得过时时间
  3. 由于链表结构因此时间切片能够作到很方便的中断和恢复
  4. 时间切片的实现是经过 settimeout + postMessage 实现的
  5. 当全部任务都延迟时会执行 clearTimeout
  6. 任务数 和 工做时间的计算

Fiber 为何要使用链表

使用链表结构只是一个结果,而不是目的,React 开发者一开始的目的是冲着模拟调用栈去的

调用栈最常常被用于存放子程序的返回地址。在调用任何子程序时,主程序都必须暂存子程序运行完毕后应该返回到的地址。所以,若是被调用的子程序还要调用其余的子程序,其自身的返回地址就必须存入调用栈,在其自身运行完毕后再行取回。除了返回地址,还会保存本地变量、函数参数、环境传递。

所以 Fiber 对象被设计成一个链表结构,经过如下主要属性组成一个链表

  • type 类型
  • return 存储当前节点的父节点
  • child 存储第一个子节点
  • sibling 存储右边第一个的兄弟节点
  • alternate 旧树的同等节点

咱们在遍历 dom 树 diff 的时候,即便中断了,咱们只须要记住中断时候的那么一个节点,就能够在下个时间片恢复继续遍历并 diff。这就是 fiber 数据结构选用链表的一大好处。

时间切片为何不用 requestIdleCallback

浏览器个周期执行的事件

1. 宏任务
  2. 微任务
  4. requestAnimationFrame
  5. IntersectionObserver
  6. 更新界面
  7. requestIdleCallback
  8. 下一帧
复制代码

根据官方描述:

window.requestIdleCallback() 方法将在浏览器的空闲时段内调用的函数排队。这使开发者可以在主事件循环上执行后台和低优先级工做,而不会影响延迟关键事件,如动画和输入响应。函数通常会按先进先调用的顺序执行,然而,若是回调函数指定了执行超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序。 你能够在空闲回调函数中调用 requestIdleCallback(),以便在下一次经过事件循环以前调度另外一个回调。

看似完美契合时间切片的思想,因此起初 React 的时间分片渲染就想要用到这个 API,不过目前浏览器支持的不给力,并且 requestIdleCallback 有点过于严格,而且执行频率不足以实现流畅的UI呈现。

并且咱们但愿经过Fiber 架构,让 reconcilation 过程变成可被中断。'适时'地让出 CPU 执行权。所以React团队不得不实现本身的版本。

实际上 Fiber 的思想和协程的概念是契合的。举个栗子:

普通函数: (没法被中断和恢复)

const tasks = []
function run() {
  let task
  while (task = tasks.shift()) {
    execute(task)
  }
}
复制代码

若是使用 Generator 语法:

const tasks = []
function * run() {
  let task

  while (task = tasks.shift()) {
    // 判断是否有高优先级事件须要处理, 有的话让出控制权
    if (hasHighPriorityEvent()) {
      yield
    }

    // 处理完高优先级事件后,恢复函数调用栈,继续执行...
    execute(task)
  }
}
复制代码

可是 React 尝试过用 Generator 实现,后来发现很麻烦,就放弃了。

为何时间切片不使用 Generator

主要是2个缘由:

  1. Generator 必须将每一个函数都包装在 Generator 堆栈中。这不只增长了不少语法开销,并且还增长了现有实现中的运行时开销。虽然有胜于无,可是性能问题仍然存在。
  2. 最大的缘由是生成器是有状态的。没法在其中途恢复。若是你要恢复递归现场,可能须要从头开始, 恢复到以前的调用栈。

时间切片为何不使用 Web Workers

是否能够经过 Web Worker 来建立多线程环境来实现时间切片呢?

React 团队也曾经考虑过,尝试提出共享的不可变持久数据结构,尝试了自定义 VM 调整等,可是 JavaScript 该语言不适用于此。

由于可变的共享运行时(例如原型),生态系统尚未作好准备,由于你必须跨工做人员重复代码加载和模块初始化。若是垃圾回收器必须是线程安全的,则它们的效率不如当前高效,而且VM实现者彷佛不肯意承担持久数据结构的实现成本。共享的可变类型数组彷佛正在发展,可是在当今的生态系统中,要求全部数据经过此层彷佛是不可行的。代码库的不一样部分之间的人为边界也没法很好地工做,而且会带来没必要要的摩擦。即便那样,你仍然有不少JS代码(例如实用程序库)必须在工做人员之间复制。这会致使启动时间和内存开销变慢。所以,是的,在咱们能够定位诸如Web Assembly之类的东西以前,线程多是不可能的。

你没法安全地停止后台线程。停止和重启线程并非很便宜。在许多语言中,它也不安全,由于你可能处于一些懒惰的初始化工做之中。即便它被有效地中断了,你也必须继续在它上面花费CPU周期。

另外一个限制是,因为没法当即停止线程,所以没法肯定两个线程是否同时处理同一组件。这致使了一些限制,例如没法支持有状态的类实例(如React.Component)。线程不能只记住你在一个线程中完成的部分工做并在另外一个线程中重复使用。

ps: 本菜不会用 React,第一次读 React 源码,对源码有误读请指正

最后

  1. 以为有用的请点个赞
  2. 本文内容出自 github.com/zhongmeizhi…
  3. 欢迎关注公众号「前端进阶课」认真学前端,一块儿进阶。回复 全栈Vue 有好礼相送哦

相关文章
相关标签/搜索