時間切片的實現和調度(原創2.6萬字)

本人系一個慣用Vue的菜雞,恰巧週末和大佬扯蛋,峯迴路轉談到了fiber,被大佬瘋狂鄙視...

大佬還和我吐槽了現在的忘了環境

  1. 百度是不可信的,百度到的東西出來廣告其他都是出自同一個作者(大部分情況確實這樣)
  2. 很多水文都是以 copy 的形式產生的,你看到的文章說不定已經過時好幾個版本了(大部分情況確實這樣)

於是本菜開始了 React Fiber 相關的讀源碼過程。爲什麼看 Fiber?因爲 Vue 沒有,Vue3 也沒有,但是卻被吹的很神奇。

本菜於編寫時間於:2020/05/25,參考的當日源碼版本 v16.13.1

Fiber的出現是爲了解決什麼問題? <略過一下>

首先必須要知道爲什麼會出現 Fiber

舊版本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秒鐘!!!(差不多 - -!)

因爲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 定義了一系列事件優先級

下面是優先級時間的源碼

[源碼文件](https://github.com/facebook/react/blob/a152827ef697c55f89926f9b6b7aa436f1c0504e/packages/scheduler/src/Scheduler.js)

  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 源碼

[源碼文件](https://github.com/facebook/react/blob/a152827ef697c55f89926f9b6b7aa436f1c0504e/packages/scheduler/src/Scheduler.js)

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 相關源碼

[源碼文件](https://github.com/facebook/react/blob/a152827ef697c55f89926f9b6b7aa436f1c0504e/packages/scheduler/src/Scheduler.js)

// 將一個任務推入任務調度隊列
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. 本文內容出自 https://github.com/zhongmeizhi/FED-note
  3. 歡迎關注公衆號「前端進階課」認真學前端,一起進階。回覆 全棧Vue 有好禮相送哦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章