React源碼解析之requestHostCallback

前言:
React源碼解析之scheduleWork(下)中,咱們講到了unstable_scheduleCallback,其中在「按計劃插入調度任務」後,會調用requestHostCallback()方法:javascript

function unstable_scheduleCallback(priorityLevel, callback, options{
    xxx
  //若是開始調度的時間已經錯過了
  if (startTime > currentTime) {
    xxx
  }
  //沒有延期的話,則按計劃插入task
  else {
    xxx
    //更新調度執行的標誌
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      /*--------------------這裏------------------*/
      //執行調度 callback
      requestHostCallback(flushWork);
      /*-------------------------------------------*/
    }
  }

}
複製代碼

本文的目的就是講解requestHostCallback()的源碼邏輯及其做用。java

1、requestHostCallback()
做用:
執行 React 的調度任務react

源碼:nginx

 //在每一幀內執行調度任務(callback)
  requestHostCallback = function(callback{
    if (scheduledHostCallback === null) {
      //firstCallbackNode 傳進來的 callback
      scheduledHostCallback = callback;
      //若是 react 在幀裏面還未超時(即多佔用了瀏覽器的時間)
      //還未開始調度
      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;
        requestAnimationFrameWithTimeout(animationTick);
      }
    }
  };
複製代碼

解析:
初始化調度任務scheduledHostCallback、調度標識isAnimationFrameScheduled,並執行requestAnimationFrameWithTimeout()方法,animationTick()函數後面解析git

2、requestAnimationFrameWithTimeout()
做用:
在每一幀內執行調度任務github

源碼:web

// This initialization code may run even on server environments if a component
// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
// have setTimeout or clearTimeout. However, we always expect them to be defined
// on the client. https://github.com/facebook/react/pull/13088
const localSetTimeout =
  typeof setTimeout === 'function' ? setTimeout : undefined;
const 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.
const localRequestAnimationFrame =
  typeof requestAnimationFrame === 'function'
    ? requestAnimationFrame
    : undefined;
const 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.
//最晚執行時間爲 100ms
const ANIMATION_FRAME_TIMEOUT = 100;
let rAFID;
let rAFTimeoutID;
//防止localRequestAnimationFrame長時間(100ms)內沒有調用,
//強制執行 callback 函數
const requestAnimationFrameWithTimeout = function(callback{
  // schedule rAF and also a setTimeout
  /*若是 A 執行了,則取消 B*/
  //就是window.requestAnimationFrame API
  //若是屏幕刷新率是 30Hz,即一幀是 33ms 的話,那麼就是每 33ms 執行一次

  //timestamp表示requestAnimationFrame() 開始去執行回調函數的時刻,是requestAnimationFrame自帶的參數
  rAFID = localRequestAnimationFrame(function(timestamp{
    // cancel the setTimeout
    //已經比 B 先執行了,就取消 B 的執行
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  //若是超過 100ms 仍未執行的話
  /*若是 B 執行了,則取消 A*/
  rAFTimeoutID = localSetTimeout(function({
    // cancel the requestAnimationFrame
    //取消 localRequestAnimationFrame
    localCancelAnimationFrame(rAFID);
    //直接調用回調函數
    callback(getCurrentTime());
      //100ms
  }, ANIMATION_FRAME_TIMEOUT);
};
複製代碼

解析:
(1)localRequestAnimationFramewindow.requestAnimationFrame
做用是和瀏覽器刷新頻率保持同步的時候,執行內部的 callback,
具體請看:window.requestAnimationFrame跨域

(2)requestAnimationFrameWithTimeout內部是兩個function
① rAFID
就是執行window.requestAnimationFrame方法,若是先執行,就清除 ② 的rAFTimeoutID瀏覽器

人眼能接受不卡頓的頻率是 30Hz,即每秒 30 幀,1 幀是 33ms,這也是 React 默認瀏覽器的刷新頻率(下文會解釋)app

也就是說,若是 ① rAFID 先執行的話,即會隨着瀏覽器刷新頻率執行,而且會阻止 ② rAFTimeoutID 的執行。

② rAFTimeoutID
rAFTimeoutID的做用更像是一個保底措施,若是 React 在進入調度流程,而且有調度隊列存在,可是 100ms 仍未執行調度任務的話,則強制執行調度任務,而且阻止 ① rAFID 的執行。

也就是說 ① rAFID 和 ② rAFTimeoutID 是「競爭關係」,誰先執行,就阻止對方執行。

執行requestAnimationFrameWithTimeout方法時,帶的參數是animationTick

requestAnimationFrameWithTimeout(animationTick);
複製代碼

接下來說下animationTick方法

3、animationTick()
做用:
計算每一幀中 react 進行調度任務的時長,並執行該 callback

源碼:

  let 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.
  let previousFrameTime = 33;
  //保持瀏覽器每秒 30 幀的狀況下,每一幀爲 33ms
  let activeFrameTime = 33;

 //計算每一幀中 react 進行調度任務的時長,並執行該 callback
  const animationTick = function(rafTime{
    //若是不爲 null 的話,當即請求下一幀重複作這件事
    //這麼作的緣由是:調度隊列有多個 callback,
    // 不能保證在一個 callback 完成後,恰好能在下一幀繼續執行下一個 callback,
    //因此在當前 callback 存在的同時,執行下一幀的 callback
    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.
      //沒有 callback 要被調度,退出
      isAnimationFrameScheduled = false;
      return;
    }
    //用來計算下一幀有多少時間是留給react 去執行調度的
    //rafTime:requestAnimationFrame執行的時間
    //frameDeadline:0 ,每一幀執行後,超出的時間
    //activeFrameTime:33,每一幀的執行事件
    let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    //若是調度執行時間沒有超過一幀時間
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime &&
      !fpsLocked
    ) {
      //React 不支持每一幀比 8ms 還要短,即 120 幀
      //小於 8ms 的話,強制至少有 8ms 來執行調度
      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);
    }
  };
複製代碼

解析:
(1)只要調度任務不爲空,則持續調用requestAnimationFrameWithTimeout(animationTick)
這樣作的目的是:
調度隊列有多個callback,不能保證在一個callback完成後,恰好能在下一幀繼續執行下一個callback,因此在當前callback存在的同時,執行下一幀的callback,以確保每一幀都有 React 的調度任務在執行。

(2)React 默認瀏覽器刷新頻率是 30Hz

  //保持瀏覽器每秒 30 幀的狀況下,每一幀爲 33ms
  let activeFrameTime = 33;
複製代碼

(3)nextFrameTime
用來計算下一幀留給 React 執行調度的時間,React 能接受最低限度的時長是 8ms,即 120Hz

(4)通知 React 調度開始執行

  //通知已經開始幀調度了
  if (!isMessageEventScheduled) {
    isMessageEventScheduled = true;
    port.postMessage(undefined);
  }
複製代碼

port.postMessage(undefined)是跨域通訊—MessageChannel的使用,接下來說一下它

4、new MessageChannel()
做用:
建立新的消息通道用來跨域通訊,而且 port1 和 port2 能夠互相通訊

使用:

  const channel = new MessageChannel();

  const port1 = channel.port1;

  const port2 = channel.port2;

  port1.onmessage = function(event{

    console.log("port111111 " + event.data);

  }

  port2.onmessage = function(event{

    console.log("port2222 " + event.data);

  }

  port1.postMessage("port1發出的");

  port2.postMessage("port2發出的");
複製代碼

結果:

port2222 port1發出的
port111111 port2發出的
複製代碼

能夠看到,port1 發出的消息被 port2 接收,port2 發出的消息被 port1 接收

React 源碼中的使用:

  // We use the postMessage trick to defer idle work until after the repaint.
  /*idleTick()*/
  const channel = new MessageChannel();
  const port = channel.port2;
  //當調用 port.postMessage(undefined) 就會執行該方法
  channel.port1.onmessage = function(event{
    isMessageEventScheduled = false;
    //有調度任務的話
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      const hasTimeRemaining = frameDeadline - currentTime > 0;
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        //仍有調度任務的話,繼續執行幀調度
        if (hasMoreWork) {
          // Ensure the next frame is scheduled.
          if (!isAnimationFrameScheduled) {
            isAnimationFrameScheduled = true;
            requestAnimationFrameWithTimeout(animationTick);
          }
        } else {
          scheduledHostCallback = null;
        }
      } catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed, and post a new task as soon as possible
        // so we can continue where we left off.
        //若是調度任務由於報錯而中斷了,React 儘量退出當前瀏覽器執行的任務,
        //繼續執行下一個調度任務
        isMessageEventScheduled = true;
        port.postMessage(undefined);
        throw error;
      }
      // Yielding to the browser will give it a chance to paint, so we can
      // reset this.
      //判斷瀏覽器是否強制渲染的標誌
      needsPaint = false;
    }
  };
複製代碼

經過port.postMessage(undefined)就會執行該方法,判斷是否有多餘的調度任務須要被執行,若是當前調度任務報錯,就會盡量繼續執行下一個調度任務。

5、綜上
本文中的函數執行及走向,不算複雜,因此不作流程圖了,能夠綜合地看到requestHostCallback()的做用是:
(1)在瀏覽器的刷新頻率(每一幀)內執行 React 的調度任務 callback
(2)計算每一幀中 React 進行調度任務的時長,多出的時間留給下一幀的調度任務,也就是維護時間片
(3)跨域通知 React 調度任務開始執行,並在調度任務 throw error 後,繼續執行下一個 調度任務。


(完)

相關文章
相關標籤/搜索