从测试看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 进行测试。

相关文章
相关标签/搜索