React scheduler详解(16.6.3)

React scheduler

什么是React scheduler呢?
这就是react可以做到在diff的时候,用来做任务分配的机制。因为js是单线程的,所以如果一次执行任务太多的话,如果在这期间用户过来点击个按钮,输入个数字什么的,浏览器可能毫无反应,这样用户可能会以为浏览器卡死啦。
现在浏览器提供了一个接口requestidlecallback, mdn描述(https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)。具体就是现在浏览器可以让我们有机会在浏览器空闲的时候,用来执行一段任务。
但是react的scheduler并没有使用该接口,而是自己实现了一个requestidlecallback的ployfill。
为什么没有这么用呢?
据说是因为兼容问题,或者是react目前没有看到浏览器厂商对它强烈的支持,或者是其他原因。
react是使用的requestAnimationFrame来模拟实现的requestidlecallback。

React scheduler 流程

这里通过把所有的任务通过双向链表链接起来,类似如下图:


image.png

然后通过requestAnimationFrame或者setTimeout来获取浏览器在每帧的空闲时间来循环处理所有的任务,直到链表为空为止。

React scheduler 代码

代码文件(node_modules/scheduler/cjs/scheduler.development.js)

unstable_scheduleCallback

// 组成双向链表,开始安排任务
    function unstable_scheduleCallback(callback, deprecated_options) {
      // currentEventStartTime 初始值为-1,也就是初始使用当前的时间
      var startTime =
        currentEventStartTime !== -1
          ? currentEventStartTime
          : exports.unstable_now();
      // 过期时间
      var expirationTime;
      // 这里很简单,就是根据不同的优先级,赋予不同的过期时间
      if (
        typeof deprecated_options === "object" &&
        deprecated_options !== null &&
        typeof deprecated_options.timeout === "number"
      ) {
        // 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 LowPriority:
            expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
            break;
          case NormalPriority:
          default:
            expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
        }
      }

      // 组装成新的node
      var newNode = {
        callback: callback,
        priorityLevel: currentPriorityLevel,
        expirationTime: expirationTime,
        next: null,
        previous: null
      };

      // Insert the new callback into the list, ordered first by expiration, then
      // by insertion. So the new callback is inserted any other callback with
      // equal expiration.
      if (firstCallbackNode === null) {
        // This is the first callback in the list.
        // 开始安排
        firstCallbackNode = newNode.next = newNode.previous = newNode;
        ensureHostCallbackIsScheduled();
      } else {
        var next = null;
        var node = firstCallbackNode;
        do {
          if (node.expirationTime > expirationTime) {
            // The new callback expires before this one.
            next = node;
            break;
          }
          node = node.next;
        } while (node !== firstCallbackNode);

        if (next === null) {
          // No callback with a later expiration was found, which means the new
          // callback has the latest expiration in the list.
          // 插在最后面
          next = firstCallbackNode;
        } else if (next === firstCallbackNode) {
          // The new callback has the earliest expiration in the entire list.
          // 插在最前面
          firstCallbackNode = newNode;
          ensureHostCallbackIsScheduled();
        }

        var previous = next.previous;
        previous.next = next.previous = newNode;
        newNode.next = next;
        newNode.previous = previous;
      }
      return newNode;
    }

这里的unstable_scheduleCallback就是requestIdleCallback的代替者,不过也可以看到源码里边标注了unstable,表示不是稳定的,以后随时会改。
unstable_scheduleCallback的功能很简单,就是根据传入的callback和options,计算出过期时间,然后组成双向任务链表,然后开始通过ensureHostCallbackIsScheduled()来安排任务循环执行。

ensureHostCallbackIsScheduled

然后来看ensureHostCallbackIsScheduled这个函数,这个也很简单,首先判断是否任务已经开始循环安排了,如果是,则退出,如果没有,则重置条件,重新开始去请求循环安排任务。

// 是否已经开始安排任务
    function ensureHostCallbackIsScheduled() {
      // 有一个callback正在进行
      if (isExecutingCallback) {
        // Don't schedule work yet; wait until the next time we yield.
        return;
      }
      // firstCallbackNode的过期时间是最早的
      // Schedule the host callback using the earliest expiration in the list.
      var expirationTime = firstCallbackNode.expirationTime;
      if (!isHostCallbackScheduled) {
        isHostCallbackScheduled = true;
      } else {
        // Cancel the existing host callback.
        // 取消其它存在的host callback
        cancelHostCallback();
      }
      // 开始安排任务队列
      requestHostCallback(flushWork, expirationTime);
    }

可以看到这里最后走到了requestHostCallback(flushWork, expirationTime);
这里的flushwork是schedule的一个刷新任务队列函数,等会再看。先看下
requestHostCallback

requestHostCallback

这里requestHostCallback根据传入的callback和过期时间确定下一步执行那些操作,如果当天正在执行任务,或者是过期时间小于0,则通过port.postMessage发送信息,来立即执行任务更新。
这里的port.postMessage是

var channel = new MessageChannel();
var port = channel.port2;

这里可以理解为一个通道,就是当在scheduler中如果想要立即执行任务链表的更新,就可以通过port.postMessage来发送一个信息,通过channel.port1.onmessage开接受信息,并且立即开始执行任务链表的更新,类似一个发布订阅,当想更新链表的时候,只需要发送个信息就可以了。
scheduler里边就是通过MessageChannel来完成通知和执行任务链表更新操作的。
requestHostCallback 里边如果没有到到期时间且还还没有开始通过isAnimationFrameScheduled来订阅浏览器的空闲时间,则通过requestAnimationFrameWithTimeout(animationTick)去订阅。

// 开始安排任务, callback就是刚才的flushwork函数, absoluteTimeout是传入的过期时间
      requestHostCallback = function(callback, absoluteTimeout) {
        // 准备开始的callback,开始执行的回调函数
        scheduledHostCallback = callback;
        // 过期时间
        timeoutTime = absoluteTimeout;
        if (isFlushingHostCallback || absoluteTimeout < 0) {
          // ASAP 尽快
          // Don't wait for the next frame. Continue working ASAP, in a new event.
          port.postMessage(undefined);
        } else if (!isAnimationFrameScheduled) {
          // 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;
          requestAnimationFrameWithTimeout(animationTick);
        }
      };

因为最后都会走到执行任务链表刷新的地方,也就是最后都会走到和port.postMessage(undefined)这里发出的请求,然后通过channel.port1.onmessage这里来处理的时候,所以这里暂时先不看这里,等到最后再看这边的代码,目前先先看下requestAnimationFrameWithTimeout

requestAnimationFrameWithTimeout

这里主要是使用requestAnimationFrame,但是会有requestAnimationFrame不起作用的情况下,使用setTimeout。

// requestAnimationFrame does not run when the tab is in the background. If
    // we're backgrounded we prefer for that work to happen so that the page
    // continues to load in the background. So we also schedule a 'setTimeout' as
    // a fallback.
    // TODO: Need a better heuristic for backgrounded work.
    var ANIMATION_FRAME_TIMEOUT = 100;
    var rAFID;
    var rAFTimeoutID;
    var requestAnimationFrameWithTimeout = function(callback) {
      // schedule rAF and also a setTimeout
      rAFID = localRequestAnimationFrame(function(timestamp) {
        // cancel the setTimeout
        localClearTimeout(rAFTimeoutID);
        callback(timestamp);
      });
      rAFTimeoutID = localSetTimeout(function() {
        // cancel the requestAnimationFrame
        localCancelAnimationFrame(rAFID);
        callback(exports.unstable_now());
      }, ANIMATION_FRAME_TIMEOUT);
    };

代码也很简单,这里传入的callback是animationTick,去看下animationTick的代码

animationTick

这个函数也很简单,就是保持循环订阅浏览器的空闲时间,同时动态的更新每帧的时间,因为react里边刚开始的默认的每帧的时间是33ms,这里也就是默认30fps,但是react里边可以根据实际的fps来动态的更新每帧的时间,通过这里,

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 =
            nextFrameTime < previousFrameTime
              ? previousFrameTime
              : nextFrameTime;

可以看到,这里react也做了一个最小的限制,最小的时候,每帧的时间是8ms

var animationTick = function(rafTime) {
        if (scheduledHostCallback !== null) {
          // Eagerly schedule the next animation callback at the beginning of the
          // frame. If the scheduler queue is not empty at the end of the frame, it
          // will continue flushing inside that callback. If the queue *is* empty,
          // then it will exit immediately. Posting the callback at the start of the
          // frame ensures it's fired within the earliest possible frame. If we
          // waited until the end of the frame to post the callback, we risk the
          // browser skipping a frame and not firing the callback until the frame
          // after that.
          requestAnimationFrameWithTimeout(animationTick);
        } else {
          // No pending work. Exit.
          isAnimationFrameScheduled = false;
          return;
        }
        // 一帧之内还剩时间rafTime - frameDeadline
        var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
        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 =
            nextFrameTime < previousFrameTime
              ? previousFrameTime
              : nextFrameTime;
        } else {
          previousFrameTime = nextFrameTime;
        }
        frameDeadline = rafTime + activeFrameTime;
        if (!isMessageEventScheduled) {
          isMessageEventScheduled = true;
          port.postMessage(undefined);
        }
      };

可以看到这里最后也是通过port.postMessage(undefined)来触发任务链表队列。

channel.port1.onmessage

现在去看下真正开始更新任务链表的时候,到底做了些什么?
代码也比较简单,最重要的就是这里调用了在开始的ensureHostCallbackIsScheduled传入的requestHostCallback(flushWork, expirationTime)的fulshWork,也就是onmessage里边的prevScheduledCallback(didTimeout)

// We use the postMessage trick to defer idle work until after the repaint.
      // 我们使用postMessage 技巧来将空闲工作推迟到重绘之后
      var channel = new MessageChannel();
      var port = channel.port2;
      channel.port1.onmessage = function(event) {
        isMessageEventScheduled = false;

        // 重制, timeout过期时间
        var prevScheduledCallback = scheduledHostCallback;
        var prevTimeoutTime = timeoutTime;
        scheduledHostCallback = null;
        timeoutTime = -1;

        var currentTime = exports.unstable_now();

        var didTimeout = false;
        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.
            // 没有执行过
            if (!isAnimationFrameScheduled) {
              // Schedule another animation callback so we retry later.
              isAnimationFrameScheduled = true;
              requestAnimationFrameWithTimeout(animationTick);
            }
            // Exit without invoking the callback.
            scheduledHostCallback = prevScheduledCallback;
            timeoutTime = prevTimeoutTime;
            return;
          }
        }

        if (prevScheduledCallback !== null) {
          isFlushingHostCallback = true;
          try {
            prevScheduledCallback(didTimeout);
          } finally {
            isFlushingHostCallback = false;
          }
        }
      };

flushWork

终于到了真正开始干活的地方啦,这里也很简单,就是通过循环,在给定的时间里去调用flushFirstCallback(),并且在最后去执行最紧急的callback

    function flushWork(didTimeout) {
      isExecutingCallback = true;
      // currentDidTimeout 初始值为false
      var previousDidTimeout = currentDidTimeout;
      currentDidTimeout = didTimeout;
      try {
        if (didTimeout) {
          // Yield 退让
          // 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 = exports.unstable_now();
            // 刷新列表
            if (firstCallbackNode.expirationTime <= currentTime) {
              do {
                flushFirstCallback();
              } while (
                firstCallbackNode !== null &&
                firstCallbackNode.expirationTime <= currentTime
              );
              continue;
            }
            break;
          }
        } else {
          // Keep flushing callbacks until we run out of time in the frame.
          if (firstCallbackNode !== null) {
            do {
              flushFirstCallback();
              // deadline < current, 空闲时间到期
            } while (firstCallbackNode !== null && !shouldYieldToHost());
          }
        }
      } finally {
        isExecutingCallback = false;
        currentDidTimeout = previousDidTimeout;
        if (firstCallbackNode !== null) {
          // There's still work remaining. Request another callback.
          ensureHostCallbackIsScheduled();
        } else {
          isHostCallbackScheduled = false;
        }
        // Before exiting, flush all the immediate work that was scheduled.
        flushImmediateWork();
      }
    }

flushFirstCallback

这里就是一些链表的操作,删除或者插入,或者重新去请求安排时间等等,

// 更新第一个任务
    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;
      if (firstCallbackNode === next) {
        // This is the last callback in the list.
        // 最后一个啦,全部设置为空
        firstCallbackNode = null;
        next = null;
      } else {
        // 从链表中删掉firstCallbackNode
        var lastCallbackNode = firstCallbackNode.previous;
        firstCallbackNode = lastCallbackNode.next = next;
        next.previous = lastCallbackNode;
      }
      // 全部设置为空,独立出来
      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 {
        continuationCallback = callback();
      } finally {
        // 换回来
        currentPriorityLevel = previousPriorityLevel;
        currentExpirationTime = previousExpirationTime;
      }

      // A callback may return a continuation. The continuation should be scheduled
      // with the same priority and expiration as the just-finished callback.
      // 有可能返回一个函数
      if (typeof continuationCallback === "function") {
        var continuationNode = {
          callback: continuationCallback,
          priorityLevel: priorityLevel,
          expirationTime: expirationTime,
          next: null,
          previous: null
        };

        // Insert the new callback into the list, sorted by its expiration. This is
        // almost the same as the code in `scheduleCallback`, except the callback
        // is inserted into the list *before* callbacks of equal expiration instead
        // of after.
        // 很简单,插入进去,根据过期时间
        if (firstCallbackNode === null) {
          // This is the first callback in the list.
          // 只有一个
          firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
        } else {
          var nextAfterContinuation = null;
          var node = firstCallbackNode;
          do {
            // 比较过期时间,
            if (node.expirationTime >= expirationTime) {
              // This callback expires at or after the continuation. We will insert
              // the continuation *before* this callback.
              nextAfterContinuation = node;
              break;
            }
            node = node.next;
          } while (node !== firstCallbackNode);
          // 如果没有,它插入到第一个
          if (nextAfterContinuation === null) {
            // 如果为空,则插入到链表的开头
            // No equal or lower priority callback was found, which means the new
            // callback is the lowest priority callback in the list.
            nextAfterContinuation = firstCallbackNode;
          } else if (nextAfterContinuation === firstCallbackNode) {
            // The new callback is the highest priority callback in the list.
            // 如果有
            firstCallbackNode = continuationNode;
            ensureHostCallbackIsScheduled();
          }

          var previous = nextAfterContinuation.previous;
          previous.next = nextAfterContinuation.previous = continuationNode;
          continuationNode.next = nextAfterContinuation;
          continuationNode.previous = previous;
        }
      }
    }

flushImmediateWork

最后更新所有最紧急的任务,

function flushImmediateWork() {
      if (
        // Confirm we've exited the outer most event handler
        currentEventStartTime === -1 &&
        firstCallbackNode !== null &&
        firstCallbackNode.priorityLevel === ImmediatePriority
      ) {
        isExecutingCallback = true;
        try {
          do {
            flushFirstCallback();
          } while (
            // Keep flushing until there are no more immediate callbacks
            firstCallbackNode !== null &&
            firstCallbackNode.priorityLevel === ImmediatePriority
          );
        } finally {
          isExecutingCallback = false;
          if (firstCallbackNode !== null) {
            // There's still work remaining. Request another callback.
            ensureHostCallbackIsScheduled();
          } else {
            isHostCallbackScheduled = false;
          }
        }
      }
    }

总结

这里的代码好像每个react版本的都会变,不过基本的原理基本上都是差不多,变得只不过是些细节。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容