scheduler 源碼

爲何是要有scheduler

首先要從js的是單線程模型來講起,Javascript執行是會經歷靜態編譯,動態解釋和事件循環作任務調度的過程,大體的流程以下(注意,該流程是以chrome瀏覽器內核爲標準的執行流程,在node或者其餘瀏覽器中,執行流程會有所差別,可是核心思想是差很少的。從這裏面咱們很直觀的認識到js的線程模型是怎麼工做的。那跟scheduler有什麼關係呢,咱們都知道React16採用了fiber架構,這樣的架構下,使用鏈表結構代替原有的函數嵌套,避免沒法控制組件渲染的過程的問題,Fiber讓React內部會動態靈活的管理全部組件的渲染任務,能夠中斷暫停某一個組件的渲染,因此,對於複雜型應用來講,對於某一個交互動做的反饋型任務,咱們是能夠對其進行拆解,一步步的作交互反饋,避免在一個頁面重繪時間週期內作過多的事情,這樣就能減小應用的長任務,最大化提高應用操做性能,更好的利用有限的時間,那麼,咱們如今能夠只聚焦在任務管理上,一塊兒來研究一下React究竟是如何調度組件的任務執行的,這裏說渲染感受不太準確

macrotask: setTimeout, setInterval, setImmediate,MessageChannel, I/O, UI rendering netWork


microtask: process.nextTick, Promises, Object.observe(廢棄), MutationObservernode

[引自大神論述]react



上圖來自知乎文章chrome

  • 主線程負責解析編譯和調度異步事件循環調度
  • 異步隊列和V8通信 經過polling Check來實現
  • 異步隊列分紅macotask 和 microtask

macrotask

通常狀況下,在沒有特別說明的狀況下咱們會把macrotask稱爲task queues ,在一次的事件循環中,他只會執行一次

microtask

  • 在每一次事件循環中,macrotask 只會提取一個執行,而 microtask 會一直提取,直到 microtasks 隊列清空。
  • 那麼也就是說若是個人某個 microtask 任務又推入了一個任務進入 microtasks 隊列,那麼在主線程完成該任務以後,仍然會繼續運行 microtasks 任務直到任務隊列耗盡
  • 而事件循環每次只會入棧一個 macrotask ,主線程執行完該任務後又會先檢查 microtasks 隊列並完成裏面的全部任務後再執行 macrotask
console.log('start');
setTimeout(function() {
  console.log('macrotask');
}, 0);
Promise.resolve().then(function() {
  console.log('microtask');
}).then(function() {
  console.log('microtask');
});
console.log(' end');

根據上述理論本身試試程序的運行結果, 爲何咱們在分析scheduler源碼以前先要介紹下異步隊列,由於瞭解清楚js異步隊列纔會讓咱們更加清晰知道scheduler是怎麼使用調度方法來更好的安排代碼執行時機。瀏覽器

瀏覽器的執行頁面繪製過程



圖片出自同一地方
執行JS(具體流程在上面有描述)--->計算Style--->構建佈局模型(Layout)--->繪製圖層樣式(Paint)--->組合計算渲染呈現結果(Composite)緩存

  • 一個完成的過程稱之爲一幀
  • 通常設備的刷新頻率是60Hz (還有更大的 scheduler 最大設定爲 120hz) 也就是按照理想狀況來講一秒鐘有60幀 那麼一幀的平均時間是 1000 / 60 大約是 16.7ms 也就是咱們一幀的執行時間不能超過 16.7 不然就會出現丟失幀和卡頓狀況
  • 幀的渲染是在處理完流程以後進行的
  • 幀的渲染是在獨立的UI線程去執行 是有GPU等
  • 剩餘的時間爲空閒時間
  • 在離散型 交互動做中不必定要求須要16.7 ms的時間
  • 對於離散型交互,上一幀的渲染到下一幀的渲染時間是屬於系統空閒時間,通過親測,Input輸入,最快的單字符輸入時間平均是33ms(經過持續按同一個鍵來觸發),至關於,上一幀到下一幀中間會存在大於16.4ms的空閒時間,就是說任何離散型交互,最小的系統空閒時間也有16.4ms,也就是說,離散型交互的最短幀長通常是33ms

requestIdleCallback

在幀的渲染中當執行完流程和UI繪製以後 會有一部分空閒時間,若是咱們能掌握這個時間加一充分利用就更加理想
那如何知道一幀進入這個空閒時間呢,瀏覽器目前提供了這個回調 requestIdleCallback 即瀏覽器空閒時
var handle = window.requestIdleCallback(callback[, options]);
  • requestIdleCallback回調調用時機是在回調註冊完成的上一幀渲染到下一幀渲染之間的空閒時間執行
  • callback 是要執行的回調函數,會傳入 deadline 對象做爲參數,deadline 包含:
  • timeRemaining:剩餘時間,單位 ms,指的是該幀剩餘時間。
  • didTimeout:布爾型,true 表示該幀裏面沒有執行回調,超時了。
  • option: {
    timeout : 即超時時間, 不提供瀏覽器本身去計算

}架構

  • 若是給定 timeout,那到了時間,無論有沒有剩餘時間,都會馬上執行回調 callback。

requestAnimationFrame

  • 之前咱們知道: requestAnimationFrame回調只會在當前頁面激活狀態下執行,能夠大大節省CPU開銷
  • requestAnimationFrame回調參數是回調被調用的時間,也就是當前幀的起始時間(能夠經過這個時間去判斷 到底有麼有超時)
  • 系統控制回調的執行時機剛好在回調註冊完成後的下一幀渲染週期的起點的開始執行,控制js計算的到屏幕響應的精確性,避免步調不一致而致使丟幀

目前瀏覽器對於requestIdleCallback的支持不是特別完整,因此react團隊放棄了requestIdleCallback的使用
本身用requestAnimationFrame和MessageChannel來polyfillapp

requestIdleCallback Polyfill方案

很簡單,33毫秒,可是時間並不老是33ms,這個時間是React認爲的一個能夠接受的最大值,若是運行設備能作到大於30fps,那麼它會去調整這個值(一般狀況下能夠調整到16.6ms)。調整策略是用當前每幀的總時間與實際每幀的時間進行比較,當實際時間小於當前時間且穩定(先後兩次都小於當前時間),那麼就會認爲這個值是有效的,而後將每幀時間調整爲該值(取先後兩次中時間大的值),還有就是requestAnimationFrame回調的第一個參數,每一幀的起始時間,最終藉助requestAnimationFrame讓一批扁平的任務剛好控制在一塊一塊的33ms這樣的時間片內執行便可異步


全部準備工做都作好了, 接下來咱們逐步來分析Scheduler源碼ide

一、 調度基本常量定義

// 枚舉

// 當即執行的任務
var ImmediatePriority = 1;

// 用戶阻塞優先級
var UserBlockingPriority = 2;

// 通常的優先級
var NormalPriority = 3;

// 低級的優先級
var LowPriority = 4;

// 空閒的優先級
var IdlePriority = 5;

// 咱們能夠理解 優先級越高 過時時間就越短 反之 越長

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
// 最大整數
var maxSigned31BitInt = 1073741823;

// Times out immediately
// 超時的優先級時間 說明沒有剩餘時間了 須要當即被調度
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
// 事件的過時時間 250 ms
var USER_BLOCKING_PRIORITY = 250;

// 通常優先級的過時時間 5000 ms
var NORMAL_PRIORITY_TIMEOUT = 5000;

// 優先級低的 10000ms
var LOW_PRIORITY_TIMEOUT = 10000;

// Never times out 空閒的任務 有沒有限制了 也就是最大整數
var IDLE_PRIORITY = maxSigned31BitInt;

二、 調度所需變量的定義

// Callbacks are stored as a circular, doubly linked list.
// 回調保存爲了循環的雙向鏈表
var firstCallbackNode = null;

// 當前是否過時
var currentDidTimeout = false;
// Pausing the scheduler is useful for debugging.

// 調度是否中斷
var isSchedulerPaused = false;

// 默認當前的優先級爲通常優先級 
var currentPriorityLevel = NormalPriority;

// 當前時間開始時間
var currentEventStartTime = -1;

// 當前過時時間
var currentExpirationTime = -1;

// This is set when a callback is being executed, to prevent re-entrancy.
// 當前是否執行callback 調度
var isExecutingCallback = false;

// 是否有回調唄調度
var isHostCallbackScheduled = false;

// 支持performance.now 函數
var hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function';

三、 調度方法 unstable_scheduleCallback

function unstable_scheduleCallback(callback, deprecated_options) {

 //開始時間
  var startTime = currentEventStartTime !== -1 ? currentEventStartTime : exports.unstable_now();

  var expirationTime;
  
  // 過時時間 這裏模擬的是 requestIdleCallback options的 timeout的定義
  // 若是這裏指定了 timeout 就會計算出 過時時間
 // 若是麼有指定就會根據 調度程序的優先級去計算  好比 普通是 5000 低級是 10000 空閒就永遠不會過時等....
  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;
    }
  }

  // 新建一個節點
  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.
  
  // 將新回調插入列表,首先按到期排序,而後按插入排序。因此新的回調插入到任何callback都擁有相同的過時時間
  
  // 若是鏈表是空的 則 從新構建
  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();
    }


    //將新的node 加入到鏈表 維護一下循環鏈表
    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}

4. ensureHostCallbackIsScheduled 遇到優先級高的 須要特別處理

function ensureHostCallbackIsScheduled() {
  
  // 調度正在執行 返回 也就是不能打斷已經在執行的
  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;
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    // 取消正在調度的callback
    cancelHostCallback();
  }
  // 發起調度
  requestHostCallback(flushWork, expirationTime);
}

五、requestAnimationFrameWithTimeout

var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
var localClearTimeout = typeof clearTimeout === 'function' ? clearTimeout : undefined;

// We don't expect either of these to necessarily be defined, but we will error
// later if they are missing on the client.
var localRequestAnimationFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : undefined;
var localCancelAnimationFrame = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;

// 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;
/**
* 是解決網頁選項卡若是在未激活狀態下requestAnimationFrame不會被觸發的問題,
*這樣的話,調度器是能夠在後臺繼續作調度的,一方面也能提高用戶體驗,
* 同時後臺執行的時間間隔是以100ms爲步長,這個是一個最佳實踐,100ms是不會影響用戶體驗同時也不影響CPU能耗的一個折中時間間隔
爲何要用 settimeout 由於requestAnimationFrame不會在tab不激活的狀況下不執行 
*/
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);
};

if (hasNativePerformanceNow) {
  var Performance = performance;
  exports.unstable_now = function () {
    return Performance.now();
  };
} else {
  exports.unstable_now = function () {
    return localDate.now();
  };
}

六、調度requestHostCallback

這裏react 作了特別的兼容處理 注入方式和不支持window或者 MessageChannel的方式 這裏不作主要分析 由於
比較簡單,這裏將主要研究現代瀏覽器的處理方式
var requestHostCallback;
var cancelHostCallback;
var shouldYieldToHost;

var globalValue = null;
if (typeof window !== 'undefined') {
  globalValue = window;
} else if (typeof global !== 'undefined') {
  globalValue = global;
}

if (globalValue && globalValue._schedMock) {
  // Dynamic injection, only for testing purposes.
  // 動態注入 用於測試目的
  var globalImpl = globalValue._schedMock;
  requestHostCallback = globalImpl[0];
  cancelHostCallback = globalImpl[1];
  shouldYieldToHost = globalImpl[2];
  exports.unstable_now = globalImpl[3];
} else if (

// 非DOM環境
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
typeof window === 'undefined' ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== 'function') {
  // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
  // fallback to a naive implementation.
  var _callback = null;
  var _flushCallback = function (didTimeout) {
    if (_callback !== null) {
      try {
        _callback(didTimeout);
      } finally {
        _callback = null;
      }
    }
  };
  requestHostCallback = function (cb, ms) {
  
  //這裏的調度就直接在settimeout 去執行 也就是直接放入macrotask隊列 這裏應該是下下策 
    if (_callback !== null) {
      // Protect against re-entrancy.
      setTimeout(requestHostCallback, 0, cb);
    } else {
      _callback = cb;
      setTimeout(_flushCallback, 0, false);
    }
  };
  cancelHostCallback = function () {
    _callback = null;
  };
  shouldYieldToHost = function () {
    return false;
  };
} else {
  if (typeof console !== 'undefined') {
    // TODO: Remove fb.me link
    if (typeof localRequestAnimationFrame !== 'function') {
      console.error("This browser doesn't support requestAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
    }
    if (typeof localCancelAnimationFrame !== 'function') {
      console.error("This browser doesn't support cancelAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
    }
  }

  // 調度的callback
  var scheduledHostCallback = null;
  // 消息發送中標識
  var isMessageEventScheduled = false;
  // 過時時間
  var timeoutTime = -1;
  // rAF 輪詢啓動狀態
  var isAnimationFrameScheduled = false;
  
  // 任務執行中標識
  var isFlushingHostCallback = false;
  // 下一幀指望完成時間點,用於判斷重繪後 js 線程是否空閒,仍是長期佔用
  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
   幀
   默認33  爲何是33 由於咱們假定每秒30幀固定評率刷新 也就是 一幀須要33ms
  */
  var previousFrameTime = 33;
  var activeFrameTime = 33;
  
  //以此推斷線程是否空閒,好添加並處理新任
  shouldYieldToHost = function () {
    return frameDeadline <= exports.unstable_now();
  };

  // We use the postMessage trick to defer idle work until after the repaint.
  // 使用postMessage 來跟蹤判斷重繪是否完成
  var channel = new MessageChannel();
  var port = channel.port2;
  
  // 當port1 發送消息後 這裏在幀重繪完成後 進入message回調 接着處理咱們
  // callback
  channel.port1.onmessage = function (event) {
  
    isMessageEventScheduled = false;
    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;

    var currentTime = exports.unstable_now();

    var didTimeout = false;
    // 說明沒有時間了 當前幀給與這個callback的時間沒有了
    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.
      // 檢查當前callback 是否過時 和 是否被執行
      // previousFrameTime 小於等於 currentTime 時,scheduler
      // 認爲線程不是空閒的,對於超時的任務將當即執行,
      // 對於未超時的任務將在下次重繪後予以處理
      // 顯然是超時的 而且沒有被取消 直接執行 而且給與timeout 爲空
      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.
        // 不執行callback 直接退出 讓輪詢去調度
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }

    // 執行callback 這裏應該是終點了 到這裏爲止 調度分析完了
    // 執行完成的調度 返回的只有 是否已通過期(didTimeout)
    if (prevScheduledCallback !== null) {
      isFlushingHostCallback = true;
      try {
        prevScheduledCallback(didTimeout);
      } finally {
        isFlushingHostCallback = false;
      }
    }
  };
 
  // 這裏做爲rAF的callback 處理函數
  /**
  * 在 animateTick 中,scheduler 將計算下一幀指望完成時間點 previousFrameTime,
  而後經過 port.postMessage 方法發送消息。等到 port1 接受到消息時,schdulear
  將 previousFrameTime 與 currentTime 做比較:當 previousFrameTime 小於等於 currentTime 時,
  scheduler 認爲線程不是空閒的,對於超時的任務將當即執行,對於未超時的任務將在下次重繪後予以處理;
  當 previousFrameTime 大於 currentTime 時,線程就是空閒的,scheduler 將當即執行。這一處理機制在
  port1.onMessage 監聽函數中實現(做爲 macrotasks,port1 接受消息的時機將隨着線程的空閒程度起變化)。
  
  */
  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;
    
    // 幀的頻率小於當前的 說明處理的時間都是比較短的
    // 其實這裏作了下調整 若是當前的設備的更新頻率大於咱們設定的 30fps 
    // 咱們就須要取更新的頻率的最大值 這裏的最大值的更新頻率 最大值 
    // 咱們須要澄清一個問題 頻率越大 一幀花費的時間就越短 
    if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
      if (nextFrameTime < 8) {
        // 預防性代碼  也就是說 不支持刷新頻率大於120hz 若是大於120 就當120處理  也就是說 一幀只有8ms
        // 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.
      
      
     //若是一幀長,那麼下一幀可能很短。
    //  若是兩個幀連續短,那麼這代表咱們實際上具備比咱們當前優化
    //的幀速率更高的幀速率。咱們相應地動態調整啓發式。例如,若是咱們是
     // 在120hz顯示屏或90hz VR顯示屏上運行。
      // 取兩個中的最大值,以防其中一個因錯過幀截止日期而異常。
      activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
      
      
    } else {
      previousFrameTime = nextFrameTime;
    }
    
    // 計算下一幀過時時間
    frameDeadline = rafTime + activeFrameTime;
    // 若是還麼發送消息 就觸發message  讓幀重繪完成後 進行調度callback
    if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;
      port.postMessage(undefined);
    }
  };


  // 
  requestHostCallback = function (callback, absoluteTimeout) {
    // 設置當前的callback
    scheduledHostCallback = callback;
    // 設置過時時間
    timeoutTime = absoluteTimeout;
    //當前若是有任務正在執行中(意爲當前沒有重繪任務,重繪線程是空閒的)
    // 或者所添加的任務須要當即執行,scheduler 直接調用 port.postMessage 發送消息,跳過 rAF 
    // 輪詢,以使任務獲得即時執行
    if (isFlushingHostCallback || absoluteTimeout < 0) {
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      port.postMessage(undefined);
    } else if (!isAnimationFrameScheduled) {
      // If rAF didn't already schedule one, we need to schedule a frame.
      // 若是raf 沒有進行調度 安排一個新的 rAF輪詢
      // 若是rAF 沒有發揮做用 在使用settimeout 去做爲預備去調度
      // 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;
      
      //若是 rAF 輪詢未啓動,調用 requestAnimationFrameWithTimeout(animationTick) 啓動輪詢
      requestAnimationFrameWithTimeout(animationTick);
    }
  };

  cancelHostCallback = function () {
    scheduledHostCallback = null;
    isMessageEventScheduled = false;
    timeoutTime = -1;
  };
}

7 執行到調度程序 callback 這裏callback是怎麼執行的(flushWork)

  • flushFirstCallback
flushFirstCallback 從雙向鏈表中取出首個任務節點並執行。若首個任務節點的 callback 返回函數,使用該函數構建新的 callbackNode 任務節點,並將該任務節點插入雙向鏈表中:若該任務節點的優先級最高、且不僅包含一個任務節點,調用 ensureHostCallbackIsScheduled,在下一次重繪後酌情執行雙向鏈表中的任務節點;不然只將新建立的任務節點添加到雙向鏈表中
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 {
    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
  //. 若是callback 返回的仍是 function 須要從新調度
  // 跟新加入一個節點是同樣的 就不在分析了
  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
基於 flushFirstCallback,flushImmediateWork 函數用於執行雙向鏈表中全部優先級爲 ImmediatePriority 的任務節點。若是雙向鏈表不僅包含優先級爲 ImmediatePriority 的任務節點,flushImmediateWork 將調用 ensureHostCallbackIsScheduled 等待下次重繪後執行剩餘的任務節點。
function flushImmediateWork() {
  if (
  // Confirm we've exited the outer most event handler
  // 確認咱們退出了最外層的事件handler
  // 執行全部當即執行的callback 
  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;
      }
    }
  }
}
  • flushWork
flushWork 做爲 requestHostCallback 函數的參數,得到的首個實參 didTimeout 爲是否超時的標識。若是超時,flushWork 經過調用 flushFirstCallback 批量執行全部未超時的任務節點;若果沒有超時,flushWork 將在下一幀未完成前(經過 shouldYieldToHost 函數判斷)儘量地執行任務節點。等上述條件邏輯執行完成後,若是雙向鏈表非空,調用 ensureHostCallbackIsScheduled 等待下次重繪後執行剩餘的任務節點。特別的,當雙向鏈表中還存在 ImmediatePriority 優先級的任務節點,flushWork 將調用 flushImmediateWork 批量執行這些任務節點。
function flushWork(didTimeout) {
  // Exit right away if we're currently paused
  // 暫停狀況下 直接退出
  if (enableSchedulerDebugging && isSchedulerPaused) {
    return;
  }

  isExecutingCallback = true;
  var previousDidTimeout = currentDidTimeout;
  currentDidTimeout = didTimeout;
  try {
    // 若是已經超時
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      // 讓firstCallbackNode 雙向鏈表去消耗
      while (firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
        // TODO Wrap in feature flag
        // 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 && !(enableSchedulerDebugging && isSchedulerPaused));
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          if (enableSchedulerDebugging && isSchedulerPaused) {
            break;
          }
          flushFirstCallback();
          // 沒有超時 也不用放入下一幀的的直接執行
        } 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();
  }
}
由於 scheduler 使用首個任務節點的超時時間點做爲 requestHostCallback 函數的次參(在 ensureHostCallbackIsScheduled 函數中處理)。所以,若是首個任務節點的優先級爲 ImmediatePriority,flushWork 所得到參數 didTimeout 也將是否值,其執行邏輯將是執行全部優先級爲 ImmediatePriority 的任務節點,再調用 ensureHostCallbackIsScheduled 等待下一次重繪時執行其他任務節點。若是首個任務節點的優先級爲 UserBlockingPriority 等,flushWork 將執行同優先級的任務節點,再調用 ensureHostCallbackIsScheduled 等待下一次重繪時執行其他任務節點。全部對不一樣優先級的任務節點,scheduler 採用分段執行的策略

八、 其餘API

  • unstable_runWithPriority
function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  var previousEventStartTime = currentEventStartTime;
  currentPriorityLevel = priorityLevel;
  currentEventStartTime = getCurrentTime();

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
    currentEventStartTime = previousEventStartTime;

    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}
unstable_runWithPriority(priorityLevel, eventHandler) 將 currentPriorityLevel 緩存設置爲 priorityLevel,隨後再執行 eventHandler,最後調用 flushImmediateWork 函數執行全部優先級爲 ImmediatePriority 的任務節點,其他任務節點等待下次重繪後再執行。能夠設想,當 eventHandler 爲 unstable_scheduleCallback 函數時,將影響所添加任務節點的優先級,並當即執行 ImmediatePriority 優先級的任務。其實就是給執行eventHandler 設置優先級
  • unstable_wrapCallback
function unstable_wrapCallback(callback) {
  var parentPriorityLevel = currentPriorityLevel;
  return function () {
    // This is a fork of runWithPriority, inlined for performance.
    var previousPriorityLevel = currentPriorityLevel;
    var previousEventStartTime = currentEventStartTime;
    currentPriorityLevel = parentPriorityLevel;
    currentEventStartTime = exports.unstable_now();

    try {
      return callback.apply(this, arguments);
    } finally {
      currentPriorityLevel = previousPriorityLevel;
      currentEventStartTime = previousEventStartTime;
      flushImmediateWork();
    }
  };
}
unstable_wrapCallback(callback) 記錄當前的優先級 currentPriorityLevel,返回函數處理效果如 unstable_runWithPriority,對於 callback 中新添加的任務節點將使用所記錄的 currentPriorityLevel 做爲優先級。
這裏能夠返回的是function 將做爲新的節點去插入被調度

9 其餘

  • unstable_pauseExecution 經過將 isSchedulerPaused 置爲 true,打斷 scheduler 處理任務節點。
  • unstable_continueExecution 取消打斷狀態,使 scheduler 恢復處理任務節點。
  • unstable_getFirstCallbackNode 獲取雙向鏈表中的首個任務節點。
  • unstable_cancelCallback(callbackNode) 從雙向鏈表中移除指定任務節點。
  • unstable_getCurrentPriorityLevel 獲取當前優先級 currentPriorityLevel 緩存。
  • unstable_shouldYield 是否須要被打斷。
  • unstable_now 獲取當前時間。

10 總結

讀完scheduler源碼 感受仍是挺複雜的 固然收穫也是比較大的 尤爲是對於瀏覽執行機制有了更深刻的認識 尤爲調度思路讓人影響時刻, 固然分析確定會有不全面或者誤差的地方 歡迎大佬們指正函數

相關文章
相關標籤/搜索