從測試看react源碼_scheduler

這是從測試看react源碼的第一篇,先從一個獨立的 Scheduler 模塊入手。正如官方所說,Scheduler模塊是一個用於協做調度任務的包,防止瀏覽器主線程長時間忙於運行一些事情,關鍵任務的執行被推遲。用於 react 內部,如今將它獨立出來,未來會成爲公開APInode

環境配置

  • 下載源碼到本地,我使用的分支是 master(4c7036e80)
  • 因爲 react 測試代碼太多,若是想要僅測試 scheduler 部分代碼,須要修改一下jest配置
  • 打開 package.json,能夠看到 test 命令的配置文件是 config.source.js
    "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js"
    複製代碼
  • 在其中咱們能夠看到又引入了 config.base.js 文件,接下來就是修改 config.base.js
    - testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$',
    + testRegex: '/__tests__/Scheduler-test.js',
    moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'],
    rootDir: process.cwd(),
    - roots: ['<rootDir>/packages', '<rootDir>/scripts'],
    + roots: ['<rootDir>/packages/scheduler'],
    複製代碼
  • 在項目根目錄下運行 yarn run test,不出意外會看到測試成功運行

jest 相關介紹

在開始以前,先了解一下jest的運行環境,省的一會找不到代碼對應位置react

  • jest.mock()用於mock某個模塊,好比jest.mock('scheduler', () => require('scheduler/unstable_mock'));, 那麼以後 require('scheduler') 其實就是引入的 scheduler/unstable_mock.js
  • 若是你看到以 hostConfig 結尾的文件名中內容是 throw new Error('This module must be shimmed by a specific build.');,那麼就去 scripts/jest/setupHostConfig.js中去查看到底使用了哪一個文件。
  • expect(xx).Function() 這裏的 Function 被稱爲 matcher。 在 jest 中有些默認的 matcher,好比說 toEqual,也能夠自定義。scripts/jest/matchers 就是一些 react 自定義的。

miniHeap

scheduler 用於調度任務,防止瀏覽器主線程長時間忙於運行一些事情,關鍵任務的執行卻被推遲。那麼任務就要有一個優先級,每次優先執行優先級最高的任務。在 schuduler 中有兩個任務隊列,taskQueue 和 timerQueue 隊列,是最小堆實現的優先隊列。數組第一項永遠爲優先級最高的子項。算法

Scheduler-test.js

1. flushes work incrementally
it('flushes work incrementally', () => {
    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('D'));

    expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
    expect(Scheduler).toFlushAndYieldThrough(['C']);
    expect(Scheduler).toFlushAndYield(['D']);
  });
複製代碼

Scheduler.unstable_yieldValue 函數比較簡單,就是將參數push到一個數組中,用於結果的比較。 scheduleCallback 中會構造 Task, 以下json

// 構造Task
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
複製代碼

並將其加入 taskQueue 或者 timerQueue。若是當前沒有任務在執行則調用 requestHostCallback(flushWork); 用於執行任務。 在測試環境中 requestHostCallback 的實現以下:數組

export function requestHostCallback(callback: boolean => void) {
  scheduledCallback = callback;
}
複製代碼

僅僅是保存了 flushWork 函數, 並無執行。 傳入的 flushWork 函數返回 boolean 變量,當 true 時爲有 task 能夠執行,可是當前時間切片已結束,將空出主線程給瀏覽器。其中執行 workLoopworkLoop 是任務執行的真正地方。其首先會從當前 timerQueue 中取出定時已經到期的timer,將其加入到 taskQueue 中。接來下就是取出 taskQueue 中的任務並執行。workLoop 代碼以下:瀏覽器

// 取出已經到時的 timer,放入 taskQueue 中。
  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') {
        // 當前任務執行過程當中被中斷,則將以後須要繼續執行的callback保存下來
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      // 已經取消的任務,調用unstable_cancelCallback方法取消任務
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
複製代碼

循環取出 taskQueue 中的任務,直到 taskQueue 爲空,或者當前時間切片時間已到而且任務也尚未過時,中斷循環。把任務放到下一個時間切片執行。注意一下 shouldYieldToHost(),這個也是判斷是否繼續執行的條件,以後會用到。bash

1-4 行代碼結果就是將四個 task 推入taskQueue,等待執行。而後去看一下 matcher toFlushAndYieldThrough, 位置在 scripts/jest/matchers/schedulerTestMatchers.js函數

function toFlushAndYieldThrough(Scheduler, expectedYields) {
  assertYieldsWereCleared(Scheduler);
  Scheduler.unstable_flushNumberOfYields(expectedYields.length);
  const actualYields = Scheduler.unstable_clearYields();
  return captureAssertion(() => {
    expect(actualYields).toEqual(expectedYields);
  });
}
複製代碼

執行與 expectedYields(就是test裏的['A', 'B'],['C'],['D'] ) 數量相同的任務,看結果是否相等 unstable_flushNumberOfYields 代碼以下,這裏的cb就是 flushWork,第一個參數爲 true表示當前切片有剩餘時間oop

export function unstable_flushNumberOfYields(count: number): void {
  if (isFlushing) {
    throw new Error('Already flushing work.');
  }
  if (scheduledCallback !== null) {
    const cb = scheduledCallback;
    expectedNumberOfYields = count;
    isFlushing = true;
    try {
      let hasMoreWork = true;
      do {
        // 執行任務
        hasMoreWork = cb(true, currentTime);
      } while (hasMoreWork && !didStop);
      if (!hasMoreWork) {
        scheduledCallback = null;
      }
    } finally {
      expectedNumberOfYields = -1;
      didStop = false;
      isFlushing = false;
    }
  }
}
複製代碼

那麼什麼時候中止呢?記得以前的 shouldYieldToHost 函數嗎,在 schedulerHostConfig.mock.js 在實現了此函數,而且添加了一個 didStop 變量用於測試中控制數量,當達到 expectedNumberOfYields 數量時退出循環。代碼以下:測試

export function shouldYieldToHost(): boolean {
  if (
    (expectedNumberOfYields !== -1 &&
      yieldedValues !== null &&
      yieldedValues.length >= expectedNumberOfYields) ||
    (shouldYieldForPaint && needsPaint)
  ) {
    // We yielded at least as many values as expected. Stop flushing.
    didStop = true;
    return true;
  }
  return false;
}
複製代碼
2. cancels work
it('cancels work', () => {
    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
    const callbackHandleB = scheduleCallback(NormalPriority, () =>
      Scheduler.unstable_yieldValue('B'),
    );
    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));

    // 取消任務即把task.callback設置爲null,
    cancelCallback(callbackHandleB);

    expect(Scheduler).toFlushAndYield([
      'A',
      // B should have been cancelled
      'C',
    ]);
  });
複製代碼

將task callback 設置爲 null 就是取消任務,這部分在 workLoop裏有判斷

3. executes the highest priority callbacks first
it('executes the highest priority callbacks first', () => {
    // 這加入taskQueue時,用最小堆排序算法排序的

    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
    scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));

    // Yield before B is flushed
    expect(Scheduler).toFlushAndYieldThrough(['A']);

    scheduleCallback(UserBlockingPriority, () =>
      Scheduler.unstable_yieldValue('C'),
    );
    scheduleCallback(UserBlockingPriority, () =>
      Scheduler.unstable_yieldValue('D'),
    );

    // C and D should come first, because they are higher priority
    expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
  });
複製代碼

在 scheduler 中定義了6中優先級:

export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
複製代碼

每種優先級又對應了不一樣的超時時間:

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
複製代碼

其中 NoPriority 和 NormalPriority 的超時時間同樣,這能夠在 scheduler.js中看到:

function timeoutForPriorityLevel(priorityLevel) {
  switch (priorityLevel) {
    case ImmediatePriority:
      return IMMEDIATE_PRIORITY_TIMEOUT;
    case UserBlockingPriority:
      return USER_BLOCKING_PRIORITY_TIMEOUT;
    case IdlePriority:
      return IDLE_PRIORITY_TIMEOUT;
    case LowPriority:
      return LOW_PRIORITY_TIMEOUT;
    case NormalPriority:
    default:
      return NORMAL_PRIORITY_TIMEOUT;
  }
}
複製代碼

在新建任務時會根據當前時間和超時時間計算出過時時間,taskQueue 是按照過時時間排序的。優先級高的任務,過時時間就會越小,因此會先被執行。

4. expires work
it('expires work', () => {
    scheduleCallback(NormalPriority, didTimeout => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue(`A (did timeout: ${didTimeout})`);
    });
    scheduleCallback(UserBlockingPriority, didTimeout => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue(`B (did timeout: ${didTimeout})`);
    });
    scheduleCallback(UserBlockingPriority, didTimeout => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue(`C (did timeout: ${didTimeout})`);
    });

    // Advance time, but not by enough to expire any work
    Scheduler.unstable_advanceTime(249);
    expect(Scheduler).toHaveYielded([]);

    // Schedule a few more callbacks
    scheduleCallback(NormalPriority, didTimeout => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue(`D (did timeout: ${didTimeout})`);
    });
    scheduleCallback(NormalPriority, didTimeout => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue(`E (did timeout: ${didTimeout})`);
    });

    // Advance by just a bit more to expire the user blocking callbacks

    // currentTime被設置成249 + 1 而 UserBlockingPriority 的timeout爲250,因此超時執行
    Scheduler.unstable_advanceTime(1);
    expect(Scheduler).toFlushExpired([
      'B (did timeout: true)',
      'C (did timeout: true)',
    ]);

    // Expire A
    // 250 + 100 + 100 + 4600 = 5050 > 5000(NORMAL_PRIORITY_TIMEOUT)
    Scheduler.unstable_advanceTime(4600);
    expect(Scheduler).toFlushExpired(['A (did timeout: true)']);

    // Flush the rest without expiring
    expect(Scheduler).toFlushAndYield([
      'D (did timeout: false)',
      'E (did timeout: true)',
    ]);
  });
複製代碼

UserBlockingPriority 類型的任務,新建 task 時設置過時時間爲當前時間 + USER_BLOCKING_PRIORITY_TIMEOUT(250), 而 NormalPriority 則爲 當前時間 + NORMAL_PRIORITY_TIMEOUT(5000)。 unstable_advanceTime 做用是就是增量當前時間。好比 unstable_advanceTime(100),意味這當前時間增長了 100ms,若是當前時間大於過時時間,則任務過時。 在最後調用 scheduledCallback 時, 代碼以下:

export function unstable_flushExpired() {
  if (isFlushing) {
    throw new Error('Already flushing work.');
  }
  if (scheduledCallback !== null) {
    isFlushing = true;
    try {
      const hasMoreWork = scheduledCallback(false, currentTime);
      if (!hasMoreWork) {
        scheduledCallback = null;
      }
    } finally {
      isFlushing = false;
    }
  }
}
複製代碼

在其中調用 scheduledCallback(也就是 flushWork) 時將 hasTimeRemaining 參數設置爲false,當前時間切片未有剩餘,僅任務過時纔會執行,未過時則放到下一個時間切片執行。這裏的第一個測試 Scheduler.unstable_advanceTime(249),有些多餘,無論設置成多少都不會有變化。 這裏須要注意的是D, E 任務過時時間爲 5249,因此最後 D 任務沒有過時 而 E 任務過時了。

5. continues working on same task after yielding
it('continues working on same task after yielding', () => {
    // workLoop 中若是callback執行以後返回函數,會把返回的函數再次存入task對象的callback中保存下來
    scheduleCallback(NormalPriority, () => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue('A');
    });
    scheduleCallback(NormalPriority, () => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue('B');
    });

    let didYield = false;
    const tasks = [
      ['C1', 100],
      ['C2', 100],
      ['C3', 100],
    ];
    const C = () => {
      while (tasks.length > 0) {
        const [label, ms] = tasks.shift();
        Scheduler.unstable_advanceTime(ms);
        Scheduler.unstable_yieldValue(label);
        if (shouldYield()) {
          didYield = true;
          return C;
        }
      }
    };

    scheduleCallback(NormalPriority, C);

    scheduleCallback(NormalPriority, () => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue('D');
    });
    scheduleCallback(NormalPriority, () => {
      Scheduler.unstable_advanceTime(100);
      Scheduler.unstable_yieldValue('E');
    });

    // Flush, then yield while in the middle of C.
    expect(didYield).toBe(false);
    expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']);
    expect(didYield).toBe(true);

    // When we resume, we should continue working on C.
    expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']);
  });
複製代碼

這裏主要看下任務C,其 callback 其最後又返回了自身,至關於任務的子任務,被中斷以後,下一個時間切片繼續執行子任務。子任務繼承了父任務的全部參數,當執行完當前子任務以後,僅僅須要設置父任務 callback 函數爲下一個子任務 callback,在 workLoop 中的相關代碼以下:

const continuationCallback = callback(didUserCallbackTimeout);
    currentTime = getCurrentTime();

    if (typeof continuationCallback === 'function') {
        // 當前任務執行過程當中被中斷,則將以後須要繼續執行的callback保存下來
        currentTask.callback = continuationCallback;
        markTaskYield(currentTask, currentTime);
    } else {
        if (enableProfiling) {
            markTaskCompleted(currentTask, currentTime);
            currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
            pop(taskQueue);
        }
    }
複製代碼

總結

目前講解了5個測試,而關於 scheduler 中的測試還有不少,更多的細節還須要本身去分析,但願本文能夠起到拋磚引玉的做用,給你起了個閱讀源碼的頭,勿急勿躁。下次會分享 schedulerBrowser-test,模擬瀏覽器環境對 scheduler 進行測試。

相關文章
相關標籤/搜索