Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reactScheduler #4

Open
lz-lee opened this issue Aug 1, 2019 · 0 comments
Open

reactScheduler #4

lz-lee opened this issue Aug 1, 2019 · 0 comments

Comments

@lz-lee
Copy link
Owner

lz-lee commented Aug 1, 2019

reactScheduler 异步任务调度

reactScheduler 核心功能

  • 维护时间片
  • 模拟浏览器 requestldleCallbackAPI (会在浏览器空闲时期依次执行回调函数)
  • 调度列表和任务超时判断

时间片概念

  • 用户感知界面流畅至少需要 1秒30帧的刷新频率(30hz), 1000 / 30, 每帧只有33ms 来执行
  • 一帧 33ms,如果react执行更新需要 20ms,那浏览器执行动画或用户反馈的时间只有 13ms,但是这一帧仍然是可以执行浏览器的动作。
  • 如果 react 更新需要 43ms, 还需要向下一帧借用 10ms, 浏览器在这第一帧中就没有时间去执行自己的任务,就会造成卡顿。
  • requestldleCallback API 会在浏览器空闲时依次调用函数, 让浏览器在每一帧都有足够的时间去执行动画或者用户反馈,防止 React 更新占用掉一帧的所用时间

scheduleCallbackWithExpirationTime 异步任务调度

requestWork 中中如果判断是异步调度的方法就会执行 scheduleCallbackWithExpirationTime

// requestWork
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  // ...
  if (expirationTime === Sync) { // 同步的调用 js 代码
    performSyncWork();
  } else { // 异步调度 独立的 react 模块包,利用浏览器有空闲的时候进行执行,设置 deadline 在此之前执行
    scheduleCallbackWithExpirationTime(root, expirationTime); // 在 secheduler 文件夹下的单独模块
  }
}


function scheduleCallbackWithExpirationTime(
  root: FiberRoot,
  expirationTime: ExpirationTime,
) {
  // callbackExpirationTime 是上一次调度的任务优先级
  if (callbackExpirationTime !== NoWork) {
    // A callback is already scheduled. Check its expiration time (timeout).
    // 当前优先级比之前正在执行的优先级低就停止
    if (expirationTime > callbackExpirationTime) {
      // Existing callback has sufficient timeout. Exit.
      return;
    } else {
      //  当前优先级更高 则取消原来那个
      if (callbackID !== null) {
        // Existing callback has insufficient timeout. Cancel and schedule a
        // new one.
        cancelDeferredCallback(callbackID);
      }
    }
    // The request callback timer is already running. Don't start a new one.
  } else {
    startRequestCallbackTimer();
  }

  // 保存当前任务优先级
  callbackExpirationTime = expirationTime;
  // originalStartTimeMs 是 react 加载的最初时间, 记录当前时间差
  const currentMs = now() - originalStartTimeMs;
  // expirationTime(到期时间为将来的某个时间)转化成 ms
  const expirationTimeMs = expirationTimeToMs(expirationTime);
  // 当前任务的过期时间, 到期时间减去当前任务开始调度的时间差,则为过期时间,如果timeout < 0 ,则表示任务过期,需要强制更新
  const timeout = expirationTimeMs - currentMs;
  // 依赖 Scheduler 模块返回的id用来cancel
  callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}

scheduleCallback

scheduler 模块下 unstable_scheduleCallback 函数

// ReactDOMHostConfig.js

export {
  unstable_now as now,
  unstable_scheduleCallback as scheduleDeferredCallback,
  unstable_cancelCallback as cancelDeferredCallback,
} from 'scheduler';
  • firstCallbackNode 是维护的双向链表结构的头部
  • 将新的任务的优先级与 firstCallbackNode 的优先级进行排序,保证firstCallbackNode 始终是优先级最高的
  • 当 firstCallbackNode 链表的首个任务改变时调用 ensureHostCallbackIsScheduled 进行调度,firstCallbackNode 没改变按原来的优先级执行
// callback -> performAsyncWork, deprecated_options -> { timeout }
function unstable_scheduleCallback(callback, deprecated_options) {
  // 即 Date.now()
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  var expirationTime;
  if (
    typeof deprecated_options === 'object' &&
    deprecated_options !== null &&
    typeof deprecated_options.timeout === 'number'
  ) {
    // expirationTime 的逻辑将来可能全部移入到 Scheduler 包中, 目前只会进入这个判断
    // FIXME: Remove this branch once we lift expiration times out of React.
    expirationTime = startTime + deprecated_options.timeout;
  } else {
    switch (currentPriorityLevel) {
      case ImmediatePriority:
        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
        break;
      case UserBlockingPriority:
        expirationTime = startTime + USER_BLOCKING_PRIORITY;
        break;
      case IdlePriority:
        expirationTime = startTime + IDLE_PRIORITY;
        break;
      case NormalPriority:
      default:
        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
    }
  }

  var newNode = {
    // performAsyncWork
    callback,
    // 目前用不到
    priorityLevel: currentPriorityLevel,
    // timeout + now()
    expirationTime,
    // 存储链表结构
    next: null,
    previous: null,
  };

  // firstCallbackNode 是用来维护的双向链表的头部
  if (firstCallbackNode === null) {
    // This is the first callback in the list.
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    // firstCallbackNode 改变需要调用  进入调度过程
    ensureHostCallbackIsScheduled();
  } else {
    // 如果有一个/多个 node 存在链表结构里
    // next表示下一个要执行的node
    var next = null;
    // 原来的
    var node = firstCallbackNode;
    do {
      // 根据 expirationTime 排序,把优先级高的放在前面,每次都跟 firstCallbackNode 比较
      if (node.expirationTime > expirationTime) {
        // The new callback expires before this one.
        // 表示新的node的优先级高于原来的,将原来的(firstCallbackNode)赋值给 next, 退出循环
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);

    // 如果 nex t为null,说明 node.expirationTime < expirationTime, 即新的node的优先级是最小的
    if (next === null) {
      // No callback with a later expiration was found, which means the new
      // callback has the latest expiration in the list.
      // next为原来的node
      next = firstCallbackNode;
    // 表示新的node的优先级 高于原来node的优先级,
    } else if (next === firstCallbackNode) {
      // The new callback has the earliest expiration in the entire list.
      // 重新将 firstCallbackNode 赋值为新的node,即保证firstCallbackNode 为优先级最高的那个node
      firstCallbackNode = newNode;
      // firstCallbackNode 改变需要调用  进入调度过程
      ensureHostCallbackIsScheduled();
    }

    // 原来 指向firstCallBakcNode.previous 和 指向firstCallBakcNode.next 都指向自己
    // previous、next 都是指向firstCallBakcNode
    var previous = next.previous;
    // 现在将 都是指向firstCallBakcNode.previous 和 都是指向firstCallBakcNode.next 指向 newNode
    previous.next = next.previous = newNode;

    // 将 newNode.previous 和newNode.next 指向firstCallBakcNode, 形成环状双向链表结构
    newNode.next = next;
    newNode.previous = previous;

  }

  return newNode;
}

ensureHostCallbackIsScheduled

  • 检查是否有 callbackNode 在执行,否则停止执行
  • 判断 hostCallback 是否在调度,已经调度就取消
  • 执行 requestHostCallback
function ensureHostCallbackIsScheduled() {
  // 表示已经有一个 callbackNode 在调用了
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  var expirationTime = firstCallbackNode.expirationTime;
  // 判断这个 hostCallback 有没有进入调度
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    cancelHostCallback();
  }
  requestHostCallback(flushWork, expirationTime);
}

requestHostCallback

// callback 为 flushWork 函数
requestHostCallback = function(callback, absoluteTimeout) {
    scheduledHostCallback = callback;
    timeoutTime = absoluteTimeout;
    // absoluteTimeout < 0 已经超时,直接强制执行
    if (isFlushingHostCallback || absoluteTimeout < 0) {
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      window.postMessage(messageKey, '*');
      // isAnimationFrameScheduled = false 表示还没进入调度循环
    } else if (!isAnimationFrameScheduled) {
      // If rAF didn't already schedule one, we need to schedule a frame.
      // TODO: If this rAF doesn't materialize because the browser throttles, we
      // might want to still have setTimeout trigger rIC as a backup to ensure
      // that we keep performing work.
      isAnimationFrameScheduled = true;
      // 进入调度, 竞争调用 animationTick
      requestAnimationFrameWithTimeout(animationTick);
    }
  };

requestAnimationFrameWithTimeout

  • 100ms 内竞争调用 requestAnimationFrame 和 setTimeout
  • 把 animationTick 加入到浏览器动画 requestAnimationFrame 回调函数里,如果 100ms 内还未执行就取消 requestAnimationFrame, 通过 setTimeout 自动执行
var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function(callback) {
  // schedule rAF and also a setTimeout
  // requestAnimationFrame 执行则取消 setTimeout
  rAFID = localRequestAnimationFrame(function(timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  // 100ms 内 requestAnimationFrame 内的 callback 还未执行,则取消requestAnimationFrame,
  rAFTimeoutID = localSetTimeout(function() {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    // 传入当前时间
    callback(getCurrentTime());
  }, ANIMATION_FRAME_TIMEOUT);
};

animationTick

  var frameDeadline = 0;
  // We start out assuming that we run at 30fps but then the heuristic tracking
  // will adjust this value to a faster fps if we get more frequent animation
  // frames.
  // 假设以 30fps 运行,然后进行跟踪,如果获得更频繁的帧,则会将此值调整为更快的fps
  var previousFrameTime = 33;
  var activeFrameTime = 33;

var animationTick = function(rafTime) {
    if (scheduledHostCallback !== null) {
      // 立马请求下一帧调用自己,不停的调用,队列里有很多 callback
      requestAnimationFrameWithTimeout(animationTick);
    } else {
      // No pending work. Exit.
      // 没有方法要被调度,返回
      isAnimationFrameScheduled = false;
      return;
    }
    //  rafTime 是调用的当前时间,frameDeadline 为0,activeFrameTime 为 33,即保持浏览器一秒 30帧,每一帧的执行一帧完整的时间
    // 用来计算下一帧可以执行的时间是多少,即nextFrameTime
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    // 下一个方法调用进来 下一帧重新计算nextFrameTime,rafTime - frameDeadline 如果小于 0 ,说明浏览器的刷新频率要大于 30hz,帧时间是要小于 33ms的
    // 如果连续两次帧的调用计算出来的时间是小于 33ms,就设置帧时间变小,
    // 主要针对不同平台的刷新频率的问题,比如 vr 120帧,根据平台的刷新频率设置 activeFrameTime, 1000 / 120 -> 8
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime
    ) {
      if (nextFrameTime < 8) {
        // Defensive coding. We don't support higher frame rates than 120hz.
        // If the calculated frame time gets lower than 8, it is probably a bug.
        // 每帧最小运行时间
        nextFrameTime = 8;
      }
      // If one frame goes long, then the next one can be short to catch up.
      // If two frames are short in a row, then that's an indication that we
      // actually have a higher frame rate than what we're currently optimizing.
      // We adjust our heuristic dynamically accordingly. For example, if we're
      // running on 120hz display or 90hz VR display.
      // Take the max of the two in case one of them was an anomaly due to
      // missed frame deadlines.
      // 通过前后帧时间的判断,来判断平台的刷新频率,然后更新 activeFrameTime, 来达到减少React运行占用时间的目的
      activeFrameTime =
        nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
    } else {
      // 前一帧运行时间和下一帧运行时间相同
      previousFrameTime = nextFrameTime;
    }
    // 第一个 frameDeadline
    frameDeadline = rafTime + activeFrameTime;
    // isMessageEventScheduled 会在 cancelHostCallback 和 idleTick 里都会设置为 false
    if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;
      // 通过 requestAnimationFrame 调用完 callback 后立马会进入浏览器动画更新的设定,往任务队列里插入react任务来模拟 requestIdleCallback 方法
      // postMessage属于 macrotask,等待浏览器执行完再执行队列里的任务,即使每一帧的时间为33ms,但等到 message 接收时,实际留给react的时间没有 33ms
      // 给任务队列插入 react 任务,等浏览器执行完自己的任务再执行这里队列里的
      window.postMessage(messageKey, '*');
    }
  };

idleTick

  • 判断 postMessage 任务
  • 判断当前帧是否把时间用完,帧时间用完且任务过期了,didTimeout 为true,标识过期,准备强制更新;任务没有过期,则退出而不执行callback
  • 帧时间没有用完,则didTimeout 为false,执行callback
  • 判断 callback 不为空,则调用 react 任务
  • 这个方法保证了用户输入或动画最大时间限度执行,react的更新任务只能在 33ms内还剩省时间或者强制更新。(超过33ms的未过期的任务继续等待)
// 接受 react 任务
window.addEventListener('message', idleTick, false);

var idleTick = function(event) {
    // 判断 key 和 判断 message 源
    if (event.source !== window || event.data !== messageKey) {
      return;
    }

    isMessageEventScheduled = false;

    // 先赋值 再重置,
    // scheduledHostCallback -> flushWork
    // timeoutTime -> firstCallbackNode.expirationTime 到期时间
    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;

    var currentTime = getCurrentTime();

    var didTimeout = false;
    // 表示浏览器动画或用户反馈操作超过 33ms, 把这一帧的时间用完了,react 没有时间去更新了
    if (frameDeadline - currentTime <= 0) {
      // There's no time left in this idle period. Check if the callback has
      // a timeout and whether it's been exceeded.
      // 表示当前任务也已经过期
      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
        // Exceeded the timeout. Invoke the callback even though there's no
        // time left.
        // 标识已过期,准备强制更新
        didTimeout = true;
      } else {
        // No timeout.
        // 没有过期,且 isAnimationFrameScheduled 为 false 的情况,则直接调用 requestAnimationFrameWithTimeout
        // 在 animationTick 函数中 判断 scheduledHostCallback === null 时才会赋值其为false,
        // 而 scheduledHostCallback === null 只有在 cancelHostCallback 函数 和 idleTick 函数中执行
        if (!isAnimationFrameScheduled) {
          // Schedule another animation callback so we retry later.
          // 调度另一个动画回调,以便我们稍后重试
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // Exit without invoking the callback.
        // 没有过期,则退出而不执行callback,将scheduledHostCallback、tiemoutTime 恢复原来值,直接return
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }

    if (prevScheduledCallback !== null) {
      // 正在调用这个 callback
      isFlushingHostCallback = true;
      try {
        // 根据 didTimeout 判断 是否强制执行更新
        prevScheduledCallback(didTimeout);
      } finally {
        isFlushingHostCallback = false;
      }
    }
  };

flushWork

  • callback 已经过期,则循环判断链表(更改firstCallbackNode)是否已过期,直到第一个没有过期的任务为止,并将已过期的任务都强制输出
  • callback 没有过期,则循环判断是否还有空余时间执行 callback,有空余时间则执行
  • 最后调用 cancelHostCallback 重置所有调度变量,其中 scheduledHostCallback重置为null 避免重复执行老的callback
var deadlineObject = {
  timeRemaining,
  didTimeout: false,
};
// 默认 hasNativePerformanceNow = true
var timeRemaining = function() {
    // ...
    // We assume that if we have a performance timer that the rAF callback
    // gets a performance timer value. Not sure if this is always true.
    // frameDeadline - now() > 0  这一帧的渲染剩余多少时间, 在 shouldYield 中使用并判断
    // 模拟传给 requestIdleCallback方法的回调函数的 IdleDeadline 参数 https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline 表示当前闲置周期的预估剩余毫秒数

    var remaining = getFrameDeadline() - performance.now();
    return remaining > 0 ? remaining : 0;
}

// didTimout -> firstCallbackNode 是否已经超时的判断
function flushWork(didTimeout) {
  // 开始调用 callback -> ensureHostCallbackIsScheduled 函数会直接return
  isExecutingCallback = true;
  deadlineObject.didTimeout = didTimeout;
  try {
    // firstCallbackNode的任务已经过期
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      // 循环
      while (firstCallbackNode !== null) {
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        var currentTime = getCurrentTime();
        // 第一个 expirationTime 肯定小于 currentTime, 为过期任务
        if (firstCallbackNode.expirationTime <= currentTime) {
          // 循环执行callback链表,直到第一个没有过期的任务为止, 已经过期的任务都强制输出
          do {
            // flushFirstCallback 会将 firstCallbackNode.next 赋值给 firstCallbackNode
            flushFirstCallback();
          } while (
            // firstCallbackNode 变成下一个,循环判断链表任务是否过期
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime
          );
          // 已经过期的任务都输出了,下一次判断if (firstCallbackNode.expirationTime <= currentTime) 为false,直接break
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      // 任务没有过期, 继续刷新回调,直到帧中的时间不足为止。
      if (firstCallbackNode !== null) {
        do {
          flushFirstCallback();
        } while (
          // 帧还有时间空闲(还有剩余时间)才执行 callback
          firstCallbackNode !== null &&
          getFrameDeadline() - getCurrentTime() > 0
        );
      }
    }
  } finally {
    isExecutingCallback = false;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      // 还剩有 firsrCallbackNode 时,调用ensureHostCallbackIsScheduled ,此时 isHostCallbackScheduled 为 true,则会执行 cancelHostCallback 重置所有的调度常量,将 scheduledHostCallback重置为null,避免在 animationTick 中将老的 callback 重复执行一边
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    // firstCallbackNode.priorityLevel === currentPriorityLevel 这个函数不会执行
    flushImmediateWork();
  }
}

flushFirstCallback

  • 将 firstCallbackNode 指向它的next处理链表表头,执行callback
function flushFirstCallback() {
  var flushedNode = firstCallbackNode;

  // Remove the node from the list before calling the callback. That way the
  // list is in a consistent state even if the callback throws.
  var next = firstCallbackNode.next;
  // 表示只有一个 callbackNode
  if (firstCallbackNode === next) {
    // This is the last callback in the list.
    firstCallbackNode = null;
    next = null;
  } else {
    // 替换 firstCallbackNode 的操作
    var lastCallbackNode = firstCallbackNode.previous;
    // 将 firstCallbackNode 指向 firstCallbackNode.next
    firstCallbackNode = lastCallbackNode.next = next;
    next.previous = lastCallbackNode;
  }

  // 清空老 firstCallbackNode 的 next 和 previous
  flushedNode.next = flushedNode.previous = null;

  // Now it's safe to call the callback.
  var callback = flushedNode.callback;
  var expirationTime = flushedNode.expirationTime;
  var priorityLevel = flushedNode.priorityLevel;
  var previousPriorityLevel = currentPriorityLevel;
  var previousExpirationTime = currentExpirationTime;
  currentPriorityLevel = priorityLevel;
  currentExpirationTime = expirationTime;
  var continuationCallback;
  try {
    // 执行 callback
    continuationCallback = callback(deadlineObject);
  } finally {
    currentPriorityLevel = previousPriorityLevel;
    currentExpirationTime = previousExpirationTime;
  }

  // performAsyncWork 没有返回则下面不会执行
   if (typeof continuationCallback === 'function') {
     // ....
   }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant