前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码解析之requestHostCallback

React源码解析之requestHostCallback

作者头像
进击的小进进
发布2019-10-08 14:23:07
9650
发布2019-10-08 14:23:07
举报
文章被收录于专栏:前端干货和生活感悟

前言: 在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()的源码逻辑及其作用。

一、requestHostCallback() 作用: 执行 React 的调度任务

源码:

代码语言:javascript
复制
 //在每一帧内执行调度任务(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()函数后面解析

二、requestAnimationFrameWithTimeout() 作用: 在每一帧内执行调度任务

源码:

代码语言:javascript
复制
// 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, 具体请看:

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame

(2)requestAnimationFrameWithTimeout内部是两个function: ① rAFID 就是执行window.requestAnimationFrame方法,如果先执行,就清除 ② 的rAFTimeoutID

人眼能接受不卡顿的频率是 30Hz,即每秒 30 帧,1 帧是 33ms,这也是 React 默认浏览器的刷新频率(下文会解释)

也就是说,如果 ① rAFID 先执行的话,即会随着浏览器刷新频率执行,并且会阻止 ② rAFTimeoutID 的执行。

② rAFTimeoutID rAFTimeoutID的作用更像是一个保底措施,如果 React 在进入调度流程,并且有调度队列存在,但是 100ms 仍未执行调度任务的话,则强制执行调度任务,并且阻止 ① rAFID 的执行。

也就是说 ① rAFID 和 ② rAFTimeoutID 是「竞争关系」,谁先执行,就阻止对方执行。

执行requestAnimationFrameWithTimeout方法时,带的参数是animationTick

代码语言:javascript
复制
requestAnimationFrameWithTimeout(animationTick);

接下来讲下animationTick方法

三、animationTick() 作用: 计算每一帧中 react 进行调度任务的时长,并执行该 callback

源码:

代码语言:javascript
复制
  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

代码语言:javascript
复制
  //保持浏览器每秒 30 帧的情况下,每一帧为 33ms
  let activeFrameTime = 33;

(3)nextFrameTime 用来计算下一帧留给 React 执行调度的时间,React 能接受最低限度的时长是 8ms,即 120Hz

(4)通知 React 调度开始执行

代码语言:javascript
复制
  //通知已经开始帧调度了
  if (!isMessageEventScheduled) {
    isMessageEventScheduled = true;
    port.postMessage(undefined);
  }

port.postMessage(undefined)是跨域通信—MessageChannel的使用,接下来讲一下它

四、new MessageChannel() 作用: 创建新的消息通道用来跨域通信,并且 port1 和 port2 可以互相通信

使用:

代码语言:javascript
复制
  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发出的");

结果:

代码语言:javascript
复制
port2222 port1发出的
port111111 port2发出的

可以看到,port1 发出的消息被 port2 接收,port2 发出的消息被 port1 接收

React 源码中的使用:

代码语言:javascript
复制
  // 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)就会执行该方法,判断是否有多余的调度任务需要被执行,如果当前调度任务报错,就会尽可能继续执行下一个调度任务。

五、综上 本文中的函数执行及走向,不算复杂,所以不做流程图了,可以综合地看到requestHostCallback()的作用是: (1)在浏览器的刷新频率(每一帧)内执行 React 的调度任务 callback (2)计算每一帧中 React 进行调度任务的时长,多出的时间留给下一帧的调度任务,也就是维护时间片 (3)跨域通知 React 调度任务开始执行,并在调度任务 throw error 后,继续执行下一个 调度任务。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档