专栏首页前端干货和生活感悟React源码解析之requestHostCallback

React源码解析之requestHostCallback

前言: 在React源码解析之scheduleWork(下)中,我们讲到了unstable_scheduleCallback,其中在「按计划插入调度任务」后,会调用requestHostCallback()方法:

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 的调度任务

源码:

 //在每一帧内执行调度任务(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() 作用: 在每一帧内执行调度任务

源码:

// 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

requestAnimationFrameWithTimeout(animationTick);

接下来讲下animationTick方法

三、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的使用,接下来讲一下它

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

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

本文分享自微信公众号 - webchen(webchen1995)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 前端的世界里没有“容易”二字

    这半年,你过得怎么样?新的热点技术学会了吗?写的代码还有bug吗?头发还好吗?还记得年初的 Flag 吗?

    桃翁
  • 【React】354- 一文吃透 React 事件机制原理

    主要分为4大块儿,主要是结合源码对 react事件机制的原理 进行分析,希望可以让你对 react事件机制有更清晰的认识和理解。

    pingan8787
  • react-navigation导航器

    和h5用a标签来跳转不太一样的是,rn必须依赖导航器跳转。导航器也可以看成是一个普通的React组件,你可以通过导航器来定义你的APP中的导航结构。导航还可以渲...

    一粒小麦
  • React中创建组件的3种方式

    那么问题来了,这三种方式有啥区别呢?这里说明一个问题,很多时候同一种效果往往有很多种实现方式,所以我们在学习的过程中要避免章节化思维,要对技术进行...

    IT人一直在路上
  • JulyNovel-React

    目前,JulyNovel后端框架基本搭建、部署完毕,GraphQL提供的API接口也有着高可用性,数据库里也存了六七百兆爬来的小说数据,是时候开始写前端了。

    从今若
  • 聊聊nacos Service的processClientBeat

    nacos-1.1.3/naming/src/main/java/com/alibaba/nacos/naming/core/Service.java

    codecraft
  • React源码解析之scheduleWork(上)

    前言: 你需要知道:浅谈React 16中的Fiber机制(https://tech.youzan.com/react-fiber/)、React源码解析之Ro...

    进击的小进进
  • react项目建立导入包问题总结

    使用react开发网页的话,我们难免会下载两个包,一个是react,一个是react-dom,其中react是react的核心代码。react的核心思想是虚拟D...

    IT人一直在路上
  • 2020年及未来的软件编程趋势预测

    2020年马上就要到了,这听起来很疯狂。似乎2020年就像是科幻小说里的故事那么遥远,但我们在这里 — 即将敲开它的大门。

    中国DevOps社区
  • 90行代码,15个元素实现无限滚动

    无限下拉加载技术使用户在大量成块的内容面前一直滚动查看。这种方法是在你向下滚动的时候不断加载新内容。

    前端劝退师

扫码关注云+社区

领取腾讯云代金券