時間切片的實現(xiàn)和調(diào)度(原創(chuàng)2.6萬字)

本人系一個慣用Vue的菜雞鲜侥,恰巧周末和大佬扯蛋,峰回路轉(zhuǎn)談到了fiber诸典,被大佬瘋狂鄙視...

大佬還和我吐槽了現(xiàn)在的忘了環(huán)境

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

于是本菜開始了 React Fiber 相關(guān)的讀源碼過程。為什么看 Fiber舀寓?因為 Vue 沒有胆数,Vue3 也沒有,但是卻被吹的很神奇基公。

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

Fiber的出現(xiàn)是為了解決什么問題? <略過一下>

首先必須要知道為什么會出現(xiàn) Fiber

舊版本React同步更新:當React決定要加載或者更新組件樹時轰豆,會做很多事胰伍,比如調(diào)用各個組件的生命周期函數(shù),計算和比對Virtual DOM酸休,最后更新DOM樹骂租。

舉個栗子:更新一個組件需要1毫秒,如果要更新1000個組件斑司,那就會耗時1秒渗饮,在這1秒的更新過程中,主線程都在專心運行更新操作宿刮。

而瀏覽器每間隔一定的時間重新繪制一下當前頁面互站。一般來說這個頻率是每秒60次。也就是說每16毫秒( 1 / 60 ≈ 0.0167 )瀏覽器會有一個周期性地重繪行為僵缺,這每16毫秒我們稱為一幀胡桃。這一幀的時間里面瀏覽器做些什么事情呢:

  1. 執(zhí)行JS。
  2. 計算Style磕潮。
  3. 構(gòu)建布局模型(Layout)翠胰。
  4. 繪制圖層樣式(Paint)。
  5. 組合計算渲染呈現(xiàn)結(jié)果(Composite)自脯。

如果這六個步驟中之景,任意一個步驟所占用的時間過長,總時間超過 16ms 了之后膏潮,用戶也許就能看到卡頓锻狗。而上述栗子中組件同步更新耗時 1秒,意味著差不多用戶卡頓了 1秒鐘O钒铡N萏贰!(差不多 - -!)

因為JavaScript單線程的特點龟糕,每個同步任務(wù)不能耗時太長,不然就會讓程序不會對其他輸入作出相應(yīng)悔耘,React的更新過程就是犯了這個禁忌讲岁,而React Fiber就是要改變現(xiàn)狀。

什么是 Fiber <略過一下>

解決同步更新的方案之一就是時間切片:把更新過程碎片化,把一個耗時長的任務(wù)分成很多小片缓艳。執(zhí)行非阻塞渲染校摩,基于優(yōu)先級應(yīng)用更新以及在后臺預(yù)渲染內(nèi)容。

Fiber 就是由 performUnitOfWork(ps:后文詳細講述) 方法操控的 工作單元阶淘,作為一種數(shù)據(jù)結(jié)構(gòu)衙吩,用于代表某些worker,換句話說溪窒,就是一個work單元坤塞,通過Fiber的架構(gòu),提供了一種跟蹤澈蚌,調(diào)度摹芙,暫停和中止工作的便捷方式。

Fiber的創(chuàng)建和使用過程:

  1. 來自render方法返回的每個React元素的數(shù)據(jù)被合并到fiber node樹中
  2. React為每個React元素創(chuàng)建了一個fiber node
  3. 與React元素不同宛瞄,每次渲染過程浮禾,不會再重新創(chuàng)建fiber
  4. 隨后的更新中,React重用fiber節(jié)點份汗,并使用來自相應(yīng)React元素的數(shù)據(jù)來更新必要的屬性盈电。
  5. 同時React 會維護一個 workInProgressTree 用于計算更新(雙緩沖),可以認為是一顆表示當前工作進度的樹杯活。還有一顆表示已渲染界面的舊樹匆帚,React就是一邊和舊樹比對,一邊構(gòu)建WIP樹的轩猩。 alternate 指向舊樹的同等節(jié)點卷扮。

PS:上文說的 workInProgress 屬于 beginWork 流程了,如果要寫下來差不多篇幅還會增加一倍均践,這就不詳細說明了...(主要是本人懶又菜...)

Fiber的體系結(jié)構(gòu)分為兩個主要階段:reconciliation(協(xié)調(diào))/render 和 commit晤锹,

React 的 Reconciliation 階段 <略過一下>

Reconciliation 階段在 Fiber重構(gòu)后 和舊版本思路差別不大, 只不過不會再遞歸去比對、而且不會馬上提交變更彤委。

涉及生命鉤子

  • shouldComponentUpdate
  • componentWillMount(廢棄)
  • componentWillReceiveProps(廢棄)
  • componentWillUpdate(廢棄)
  • static getDerivedStateFromProps

reconciliation 特性:

  • 可以打斷鞭铆,在協(xié)調(diào)階段如果時間片用完,React就會選擇讓出控制權(quán)焦影。因為協(xié)調(diào)階段執(zhí)行的工作不會導致任何用戶可見的變更车遂,所以在這個階段讓出控制權(quán)不會有什么問題凤薛。
  • 因為協(xié)調(diào)階段可能被中斷牺氨、恢復(fù)奥裸,甚至重做挨摸,React 協(xié)調(diào)階段的生命周期鉤子可能會被調(diào)用多次!, 例如 componentWillMount 可能會被調(diào)用兩次镀钓。
  • 因此協(xié)調(diào)階段的生命周期鉤子不能包含副作用冤竹,所以财饥,該鉤子就被廢棄了

完成 reconciliation 過程落竹。這里用的是 深度優(yōu)先搜索(DFS),先處理子節(jié)點剪况,再處理兄弟節(jié)點教沾,直到循環(huán)完成。

React 的 Commit 階段 <略過一下>

涉及生命鉤子

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount(廢棄)
  • getSnapshotBeforeUpdate

rendercommit:不能暫停译断,會一直更新界面直到完成

Fiber 如何處理優(yōu)先級授翻?

對于UI來說需要考慮以下問題:

并不是所有的state更新都需要立即顯示出來,比如:

  • 屏幕之外的部分的更新并不是所有的更新優(yōu)先級都是一樣的
  • 用戶輸入的響應(yīng)優(yōu)先級要比通過請求填充內(nèi)容的響應(yīng)優(yōu)先級更高
  • 理想情況下孙咪,對于某些高優(yōu)先級的操作堪唐,應(yīng)該是可以打斷低優(yōu)先級的操作執(zhí)行的

所以,React 定義了一系列事件優(yōu)先級

下面是優(yōu)先級時間的源碼

[源碼文件](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;

當有更新任務(wù)來的時候该贾,不會馬上去做 Diff 操作羔杨,而是先把當前的更新送入一個 Update Queue 中,然后交給 Scheduler 去處理杨蛋,Scheduler 會根據(jù)當前主線程的使用情況去處理這次 Update兜材。

不管執(zhí)行的過程怎樣拆分、以什么順序執(zhí)行逞力,F(xiàn)iber 都會保證狀態(tài)的一致性和視圖的一致性曙寡。

如何保證相同在一定時間內(nèi)觸發(fā)的優(yōu)先級一樣的任務(wù)到期時間相同? React 通過 ceiling 方法來實現(xiàn)的寇荧。举庶。。本菜沒使用過 | 語法...

下面是處理到期時間的 ceiling 源碼

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

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

那么為什么需要保證時間一致性揩抡?請看下文户侥。

Fiber 如何調(diào)度?

首先要找到調(diào)度入口地址 scheduleUpdateOnFiber峦嗤,

每一個root都有一個唯一的調(diào)度任務(wù)蕊唐,如果已經(jīng)存在,我們要確保到期時間與下一級別任務(wù)的相同(所以用上文提到的 ceiling 方法來控制到期時間)

源碼文件

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

  // 調(diào)用markUpdateTimeFromFiberToRoot烁设,更新 fiber 節(jié)點的 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還會讀取優(yōu)先級。
  // 將優(yōu)先級作為參數(shù)傳遞給該函數(shù)和該函數(shù)装黑。
  const priorityLevel = getCurrentPriorityLevel();

  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates
      // 檢查是否在未批處理的更新內(nèi)
      (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.
      // 在根上注冊待處理的交互副瀑,以避免丟失跟蹤的交互數(shù)據(jù)。
      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.
        // 推入調(diào)度任務(wù)隊列
        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. 調(diào)用 markUpdateTimeFromFiberToRoot 更新 Fiber 節(jié)點的 expirationTime
  2. ensureRootIsScheduled(更新重點)
  3. schedulePendingInteractions 實際上會調(diào)用 scheduleInteractions
  • scheduleInteractions 會利用FiberRoot的 pendingInteractionMap 屬性和不同的 expirationTime恋谭,獲取每次schedule所需的update任務(wù)的集合糠睡,記錄它們的數(shù)量,并檢測這些任務(wù)是否會出錯疚颊。

更新的重點在于 scheduleUpdateOnFiber 每一次更新都會調(diào)用 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 主要是根據(jù)同步/異步狀態(tài)做不同的 push 功能铜幽。

同步調(diào)度 function scheduleSyncCallback(callback: SchedulerCallback)

  • 如果隊列不為空就推入同步隊列(syncQueue.push(callback)
  • 如果為空就立即推入 任務(wù)調(diào)度隊列(Scheduler_scheduleCallback)
  • 會將 performSyncWorkOnRoot 作為 SchedulerCallback

下面是 scheduleSyncCallback 源碼內(nèi)容

源碼文件

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;
}

異步調(diào)度滞谢,異步的任務(wù)調(diào)度很簡單串稀,直接將異步任務(wù)推入調(diào)度隊列(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);
}

不管同步調(diào)度還是異步調(diào)度,都會經(jīng)過 Scheduler_scheduleCallback 也就是調(diào)度的核心方法 function unstable_scheduleCallback(priorityLevel, callback, options)母截,它們會有各自的 SchedulerCallback

小提示:由于下面很多代碼中會使用 peek到忽,先插一段 peek 實現(xiàn),其實就是返回數(shù)組中的第一個 或者 null

peek 相關(guān)源碼文件

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

下面是 Scheduler_scheduleCallback 相關(guān)源碼

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

// 將一個任務(wù)推入任務(wù)調(diào)度隊列
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 {
    // 針對不同的優(yōu)先級算出不同的過期時間
    timeout = timeoutForPriorityLevel(priorityLevel);
    startTime = currentTime;
  }
  
   // 定義新的過期時間
  var expirationTime = startTime + timeout;

  // 定義一個新的任務(wù)
  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;

    // 將超時的任務(wù)推入超時隊列
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      // 當所有任務(wù)都延遲時清寇,而且該任務(wù)是最早的任務(wù)
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;

    // 將新的任務(wù)推入任務(wù)隊列
    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.
    // 執(zhí)行回調(diào)方法喘漏,如果已經(jīng)再工作需要等待一次回調(diào)的完成
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
        (flushWork);
    }
  }

  return newTask;
}

小提示: markTaskStart 主要起到記錄的功能,對應(yīng)的是 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
  • 如果任務(wù)已過期
    • 將超時任務(wù)推入超時隊列
    • 如果所有任務(wù)都延遲時华烟,而且該任務(wù)是最早的任務(wù)翩迈,會調(diào)用 cancelHostTimeout
    • 調(diào)用 requestHostTimeout
  • 將新任務(wù)推入任務(wù)隊列

源碼文件

補上 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. 調(diào)用 advanceTimers 檢查不再延遲的任務(wù)负饲,并將其添加到隊列中。

下面是 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. 調(diào)用 requestHostCallback 通過 MessageChannel 的異步方法來開啟任務(wù)調(diào)度 performWorkUntilDeadline

requestHostCallback 這個方法特別重要

源碼文件

// 通過onmessage 調(diào)用 performWorkUntilDeadline 方法
channel.port1.onmessage = performWorkUntilDeadline;

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

然后是同文件下的 performWorkUntilDeadline喂链,調(diào)用了 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 主要的作用是調(diào)用 workLoop 去循環(huán)執(zhí)行所有的任務(wù)

源碼文件

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 在一個文件中返十,作用是從調(diào)度任務(wù)隊列中取出優(yōu)先級最高的任務(wù),然后去執(zhí)行椭微。

還記得上文講的 SchedulerCallback 嗎洞坑?

  • 對于同步任務(wù)執(zhí)行的是 performSyncWorkOnRoot
  • 對于異步的任務(wù)執(zhí)行的是 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 操作。

這個方法只不過異步的方法是可以打斷的蝇率,我們每次調(diào)用都要查看是否超時迟杂。

源碼文件

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相關(guān)源碼文件

總結(jié)

最后是總結(jié)部分攻泼,該不該寫這個想了很久,每個讀者在不同時間不同心境下看源碼的感悟應(yīng)該是不一樣的(當然自己回顧的時候也是讀者)鉴象。每次看應(yīng)該都有每個時期的總結(jié)忙菠。

但是如果不寫總結(jié),這篇解析又感覺枯燥無味纺弊,且沒有結(jié)果牛欢。所以簡單略過一下(肯定是原創(chuàng)啦,別的地方?jīng)]有的)

  1. fiber其實就是一個節(jié)點淆游,是鏈表的遍歷形式
  2. fiber 通過優(yōu)先級計算 expirationTime 得到過期時間
  3. 因為鏈表結(jié)構(gòu)所以時間切片可以做到很方便的中斷和恢復(fù)
  4. 時間切片的實現(xiàn)是通過 settimeout + postMessage 實現(xiàn)的
  5. 當所有任務(wù)都延遲時會執(zhí)行 clearTimeout
  6. 任務(wù)數(shù) 和 工作時間的計算

Fiber 為什么要使用鏈表

使用鏈表結(jié)構(gòu)只是一個結(jié)果傍睹,而不是目的隔盛,React 開發(fā)者一開始的目的是沖著模擬調(diào)用棧去的

調(diào)用棧最經(jīng)常被用于存放子程序的返回地址。在調(diào)用任何子程序時拾稳,主程序都必須暫存子程序運行完畢后應(yīng)該返回到的地址吮炕。因此,如果被調(diào)用的子程序還要調(diào)用其他的子程序访得,其自身的返回地址就必須存入調(diào)用棧龙亲,在其自身運行完畢后再行取回。除了返回地址悍抑,還會保存本地變量鳄炉、函數(shù)參數(shù)、環(huán)境傳遞搜骡。

因此 Fiber 對象被設(shè)計成一個鏈表結(jié)構(gòu)拂盯,通過以下主要屬性組成一個鏈表

  • type 類型
  • return 存儲當前節(jié)點的父節(jié)點
  • child 存儲第一個子節(jié)點
  • sibling 存儲右邊第一個的兄弟節(jié)點
  • alternate 舊樹的同等節(jié)點

我們在遍歷 dom 樹 diff 的時候,即使中斷了记靡,我們只需要記住中斷時候的那么一個節(jié)點谈竿,就可以在下個時間片恢復(fù)繼續(xù)遍歷并 diff。這就是 fiber 數(shù)據(jù)結(jié)構(gòu)選用鏈表的一大好處簸呈。

時間切片為什么不用 requestIdleCallback

瀏覽器個周期執(zhí)行的事件

  1. 宏任務(wù)
  2. 微任務(wù)
  4. requestAnimationFrame
  5. IntersectionObserver
  6. 更新界面
  7. requestIdleCallback
  8. 下一幀

根據(jù)官方描述:

window.requestIdleCallback() 方法將在瀏覽器的空閑時段內(nèi)調(diào)用的函數(shù)排隊榕订。這使開發(fā)者能夠在主事件循環(huán)上執(zhí)行后臺和低優(yōu)先級工作,而不會影響延遲關(guān)鍵事件蜕便,如動畫和輸入響應(yīng)劫恒。函數(shù)一般會按先進先調(diào)用的順序執(zhí)行,然而轿腺,如果回調(diào)函數(shù)指定了執(zhí)行超時時間 timeout两嘴,則有可能為了在超時前執(zhí)行函數(shù)而打亂執(zhí)行順序。
你可以在空閑回調(diào)函數(shù)中調(diào)用 requestIdleCallback()族壳,以便在下一次通過事件循環(huán)之前調(diào)度另一個回調(diào)憔辫。

看似完美契合時間切片的思想,所以起初 React 的時間分片渲染就想要用到這個 API仿荆,不過目前瀏覽器支持的不給力贰您,而且 requestIdleCallback 有點過于嚴格,并且執(zhí)行頻率不足以實現(xiàn)流暢的UI呈現(xiàn)拢操。

而且我們希望通過Fiber 架構(gòu)锦亦,讓 reconcilation 過程變成可被中斷。'適時'地讓出 CPU 執(zhí)行權(quán)令境。因此React團隊不得不實現(xiàn)自己的版本杠园。

實際上 Fiber 的思想和協(xié)程的概念是契合的。舉個栗子:

普通函數(shù): (無法被中斷和恢復(fù))

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

如果使用 Generator 語法:

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

  while (task = tasks.shift()) {
    // 判斷是否有高優(yōu)先級事件需要處理, 有的話讓出控制權(quán)
    if (hasHighPriorityEvent()) {
      yield
    }

    // 處理完高優(yōu)先級事件后舔庶,恢復(fù)函數(shù)調(diào)用棧抛蚁,繼續(xù)執(zhí)行...
    execute(task)
  }
}

但是 React 嘗試過用 Generator 實現(xiàn)陈醒,后來發(fā)現(xiàn)很麻煩,就放棄了瞧甩。

為什么時間切片不使用 Generator

主要是2個原因:

  1. Generator 必須將每個函數(shù)都包裝在 Generator 堆棧中钉跷。這不僅增加了很多語法開銷,而且還增加了現(xiàn)有實現(xiàn)中的運行時開銷亲配。雖然有勝于無尘应,但是性能問題仍然存在。
  2. 最大的原因是生成器是有狀態(tài)的吼虎。無法在其中途恢復(fù)。如果你要恢復(fù)遞歸現(xiàn)場苍鲜,可能需要從頭開始, 恢復(fù)到之前的調(diào)用棧思灰。

時間切片為什么不使用 Web Workers

是否可以通過 Web Worker 來創(chuàng)建多線程環(huán)境來實現(xiàn)時間切片呢?

React 團隊也曾經(jīng)考慮過混滔,嘗試提出共享的不可變持久數(shù)據(jù)結(jié)構(gòu)洒疚,嘗試了自定義 VM 調(diào)整等,但是 JavaScript 該語言不適用于此坯屿。

因為可變的共享運行時(例如原型)油湖,生態(tài)系統(tǒng)還沒有做好準備,因為你必須跨工作人員重復(fù)代碼加載和模塊初始化领跛。如果垃圾回收器必須是線程安全的乏德,則它們的效率不如當前高效,并且VM實現(xiàn)者似乎不愿意承擔持久數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)成本吠昭。共享的可變類型數(shù)組似乎正在發(fā)展喊括,但是在當今的生態(tài)系統(tǒng)中,要求所有數(shù)據(jù)通過此層似乎是不可行的矢棚。代碼庫的不同部分之間的人為邊界也無法很好地工作郑什,并且會帶來不必要的摩擦。即使那樣蒲肋,你仍然有很多JS代碼(例如實用程序庫)必須在工作人員之間復(fù)制蘑拯。這會導致啟動時間和內(nèi)存開銷變慢。因此兜粘,是的申窘,在我們可以定位諸如Web Assembly之類的東西之前,線程可能是不可能的妹沙。

你無法安全地中止后臺線程偶洋。中止和重啟線程并不是很便宜。在許多語言中距糖,它也不安全玄窝,因為你可能處于一些懶惰的初始化工作之中牵寺。即使它被有效地中斷了,你也必須繼續(xù)在它上面花費CPU周期恩脂。

另一個限制是帽氓,由于無法立即中止線程,因此無法確定兩個線程是否同時處理同一組件俩块。這導致了一些限制黎休,例如無法支持有狀態(tài)的類實例(如React.Component)。線程不能只記住你在一個線程中完成的部分工作并在另一個線程中重復(fù)使用玉凯。

ps: 本菜不會用 React势腮,第一次讀 React 源碼,對源碼有誤讀請指正

最后

  1. 覺得有用的請點個贊
  2. 本文內(nèi)容出自 https://github.com/zhongmeizhi/FED-note
  3. 歡迎關(guān)注公眾號「前端進階課」認真學前端漫仆,一起進階捎拯。回復(fù) 全棧Vue 有好禮相送哦
image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盲厌,一起剝皮案震驚了整個濱河市署照,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吗浩,老刑警劉巖建芙,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異懂扼,居然都是意外死亡禁荸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門微王,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屡限,“玉大人,你說我怎么就攤上這事炕倘【螅” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵罩旋,是天一觀的道長啊央。 經(jīng)常有香客問我,道長涨醋,這世上最難降的妖魔是什么瓜饥? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮浴骂,結(jié)果婚禮上乓土,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好趣苏,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布狡相。 她就那樣靜靜地躺著,像睡著了一般食磕。 火紅的嫁衣襯著肌膚如雪尽棕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天彬伦,我揣著相機與錄音滔悉,去河邊找鬼。 笑死单绑,一個胖子當著我的面吹牛回官,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播询张,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼孙乖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了份氧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤弯屈,失蹤者是張志新(化名)和其女友劉穎蜗帜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體资厉,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡厅缺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了宴偿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片湘捎。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖窄刘,靈堂內(nèi)的尸體忽然破棺而出窥妇,到底是詐尸還是另有隱情,我是刑警寧澤娩践,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布活翩,位于F島的核電站,受9級特大地震影響翻伺,放射性物質(zhì)發(fā)生泄漏材泄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一吨岭、第九天 我趴在偏房一處隱蔽的房頂上張望拉宗。 院中可真熱鬧,春花似錦、人聲如沸旦事。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽族檬。三九已至歪赢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間单料,已是汗流浹背埋凯。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扫尖,地道東北人白对。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像换怖,于是被迫代替她去往敵國和親甩恼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359