从中断机制看 React Fiber 技术

Chase620 等级 363 0 0

带你了解计算机的中断机制(操作系统心脏)是如何提在 React Fiber 中应用及提高了页面渲染性能和用户体验。 前言 React 16 开始,采用了 Fiber 机制替代了原有的同步渲染 VDOM 的方案,提高了页面渲染性能和用户体验。Fiber 究竟是什么,网上也有很多优秀的技术揭秘文章,本篇主要想从计算机的中断机制来聊聊 React Fiber 技术大概工作原理。

单任务

在早期的单任务系统上,用户一次只能提交一个任务,当前运行的任务拥有全部硬件和软件资源,如果任务不主动释放 CPU 控制权,那么将一直占用所有资源,可能影响其他任务,造成资源浪费。该模式非常像当前浏览器运行模式,由于 UI 线程和 JS 线程的运行是互斥的,一旦 JS 长时间执行,浏览器无法及时响应用户交互,很容造成界面的卡顿,React 早期的同步渲染机制,当一次性更新的节点太多时,容易影响用户体验。

中断

中断最初是用于提高处理器效率的一种手段,在没有中断的情况下,当 CPU 在执行一段代码时,如果程序不主动退出(如:一段无限循环代码),那么 CPU 将被一直占用,影响其他任务运行。

  while(true) {
  ...
};

而中断机制会强制中断当前 CPU 所执行的代码,转而去执行先前注册好的中断服务程序。比较常见的如:时钟中断,它每隔一定时间将中断当前正在执行的任务,并立刻执行预先设置的中断服务程序,从而实现不同任务之间的交替执行,这也是在多任务系统的重要的基础机制。中断机制主要通过硬件触发,CPU 属于被动接受。有了中断后,各任务执行时间就可以得到非常好的控制。

回到浏览器,目前浏览器大多是 60Hz(60帧/秒),既每一帧耗时大概在 16ms 左右,它会经过下面这几个过程:

输入事件处理

  • requestAnimationFrame
  • DOM 渲染
  • RIC (RequestIdelCallback)

我们除了在步骤1-3的中进行加塞外,无法进行任何干预,而步骤4的 RIC,算是一种防止多余计算资源被浪费的机制,例如,当一帧中步骤1-3只耗费 6ms,那么剩余 10ms 的计算资源则会被浪费,而 RIC 就是浏览器提供的一种资源利用的接口。RIC 非常像前面提到的“中断服务”,而浏览器的每一帧类似“中断机制”,利用它则可以在实现我们前面提到的大任务卡顿问题,例如:之前我们在 JS 中写如下代码时,无疑会阻塞浏览器渲染,:

  function task(){
  while(true){
   ...
  };
}

task(); 但利用 RIC 机制后,我们完全可以让大任务周期性的执行,从而不阻止浏览器正常渲染。

将上面示例代码根据 RequestIdelCallback 调整下,如:

  function task(){
  while(true){
   ...
  };
}


requestIdleCallback(task);

遗憾的是,由于我们的代码运行在用户态,无法感知到底层的真实中断,我们现在利用的 RIC 也只是一种中断的近似模拟,以上代码并不会在 16ms 到期后被强制中断,我们只能主动进行释放,将控制权交还浏览器,RIC 提供了 timeRemaining 方法,让任务知道主动释放时机,我们调整以上代码,如下:

  function task(deadline){
  while(true){
   ...
   if(!deadline.timeRemaining()) {
     requestIdleCallback(task);
     // 主动退出循环,将控制权交还浏览器
     break;
   }
  };
}


requestIdleCallback(task);

以上示例,可以让一个大循环在“中断”机制下,不阻塞浏览器的渲染和响应。

注意:RIC 调用频率大概是 20 次/秒,远远低于页面流畅度的要求!这样每次你能得到差不多 50ms 的计算时间,如果完全用这 50ms 来做计算,同样会带来交互上的卡顿,所以 React Fiber 是基于自定义一套机制来模拟实现,如:setTimeout、setImmediate、MessageChannel。

以下是React Fiber中的主动释放片段代码:

  function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 如果超时,则主动退出循环,将控制权交还浏览器
      break;
    }
    ...
  }
  ...
}

调度任务 有了中断机制,中断服务后,不同任务就能实现间断执行的可能,如何实现多任务的合理调度,就需要一个调度任务来进行处理,这通常代表着操作系统。例如,当一个任务A在执行到一半时,被中断机制强制中断,此时操作系统需要对当前任务A进行现场保护,如:寄存器数据,然后切换到下一个任务B,当任务A再次被调度时,操作系统需要还原之前任务A的现场信息,如:寄存器数据,从而保证任务A能继续执行下一半任务。调度过程中如何保证被中断任务的信息不被破坏是一个非常重要的功能。

浏览器提供的 RIC 机制,类似“中断服务”注册机制,注册后我们只要合适的时机进行释放,就能实现“中断”效果,刚也提到对于不同任务之间切换,在中断后,需要考虑现场保护和现场还原。早期 React 是同步渲染机制,实际上是一个递归过程,递归可能会带来长的调用栈,这其实会给现场保护和还原变得复杂,React Fiber 的做法将递归过程拆分成一系列小任务(Fiber),转换成线性的链表结构,此时现场保护只需要保存下一个任务结构信息即可,所以拆分的任务上需要扩展额外信息,该结构记录着任务执行时所需要的必备信息:

  {
    stateNode,
    child,
    return,
    sibling,
    expirationTime
    ...
}

我们看以下示例代码:

  ReactDOM.render(
<div id="A">
  A
  <div id="B">
    B
     <div id="C">C</div>
  </div>
  <div id="D">
      D
  </div>
</div>, 
node)

当 React 进行渲染时,会生成如下任务链,此时如果在执行任务B后时发现时间不足,主动释放后,只需要记录下一次任务C的信息,等再次调度时取得上次记录的信息即可。使用该机制后,对于渲染任务的优先级、撤销、挂起、恢复都能得到非常好的控制。

总结 中断机制其实是一种非常重要的解决资源共享的手段,对于操作系统而言,它已经是一个必不可少功能。随着浏览器的功能越来越强,越来越多功能也搬到了浏览器,如何保证用户在使用过程中的流畅,也是经常需要思考的问题,在业务开发过程中,我们可以根据实际场景利用好“中断机制”,提高用户体验。

收藏
评论区

相关推荐

为什么 React 源码不用 TypeScript 来写?
周末的,看点轻松的吧,之前看过 React 的源码,比较好奇像 React 这样庞大的工程为什么没有用 TypeScript。 Facebook 工程师 Cat Chen 在知乎上(https://www.zhihu.com/question/378470381/answer/1079675543(https://www.zhihu.com/quest
《精通react/vue组件设计》之配合React Portals实现一个功能强大的抽屉(Drawer)组件
前言 本文是笔者写组件设计的第六篇文章,内容依次从易到难,今天会用到react的高级API React Portals,它也是很多复杂组件必用的方法之一. 通过组件的设计过程,大家会接触到一个完成健壮的组件设计思路和方法,也能在实现组件的过程逐渐对react/vue的高级知识和技巧有更深的理解和掌握,并且在企业实际工作做游刃有余. 之所以会写组件设计相关
25、react入门教程
25、react入门教程 25、react入门教程 0. React介绍 0.1 什么是React? React(有时称为React.js 或ReactJS)是一
Create React App
Create React App Create React App Facebook开源了React前端框架(MIT Licence),也同时提供了React脚手架 createreactapp(ht
一起走进React核心团队
当我刚来Facebook的React团队工作时,我不确定接下来的工作会怎么样。表面看,React核心团队似乎很大!但事实证明,像Eli White和Sebastian McKenzie这样的人都在React Native团队。考虑加上那些维护开源库的维护者,比如Chakra UI、Framer Motion,React核心团队人数似乎能填满整个体育场!但事实
Hook 简介 – React
Hook 简介 _Hook_ 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。 import React, { useState } from 'react'; function Example() { // 声明一个新的叫做 “count” 的 sta
从中断机制看 React Fiber 技术
带你了解计算机的中断机制(操作系统心脏)是如何提在 React Fiber 中应用及提高了页面渲染性能和用户体验。 前言 React 16 开始,采用了 Fiber 机制替代了原有的同步渲染 VDOM 的方案,提高了页面渲染性能和用户体验。Fiber 究竟是什么,网上也有很多优秀的技术揭秘文章,本篇主要想从计算机的中断机制来聊聊 React Fiber 技术
使用 State Hook – React
使用 State Hook_Hook_ 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。中使用下面的例子介绍了 Hook:import React, { useState } from 'react';function Example() { //
使用 Effect Hook – React
使用 Effect Hook_Hook_ 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。_Effect Hook_ 可以让你在函数组件中执行副作用操作import React, { useState, useEffect } from 'reac
React 之设计原则
编写该文档的目的是,使开发者更易于了解我们如何决策 React(应该做哪些,不应该做哪些),以及我们的开发理念。我们非常欢迎来自社区的贡献,但如若违背这些理念,实非我们所愿。 注意: 文章描述了 React 自身的设计原则,而非 React 组件或应用,阅读者需要对 React 有深入的理解。 如需 React 的入门文档,查看 。
React 之源码概览
本节将对 React 的源码架构,约定及其实现进行概述。如果您想 的开发,我们希望这份指南可以帮助你更加轻松地进行修改。我们并不推荐在 React 应用中遵循这些约定。有许多约定是历史原因,并且之后也许会有所修改。 项目根目录当克隆 之后,你们将看到项目根目录的信息: 包含元数据(比如 package.json)和 React 仓库中所有
Omi架构与React Fiber
写在前面 Omi框架(https://github.com/AlloyTeam/omi)在架构设计的时候就决定把update的控制权交给了开发者,视灵活性比生命还重要。不然的话,如果遇到React Fiber要解决的这类问题的话,就需要
React - 认识生命周期
这周开始学习React的生命周期。React的生命周期从广义上分为三个阶段:挂载、渲染、卸载. 挂载卸载过程 constructor() componentWillMount() componentDidMount() componentWillUnmount() 更新过程 componentWillRece
面向初学者的 React 路由-React Router的完整指南!
因此,您正在尝试学习React.js。也许您甚至已经在其中构建了几个简单的项目。无论您是新开发人员还是有一定经验的人,都可能会发现自己必须开发具有不同页面和路线的Web应用程序。那就是React Router发挥作用的时候。什么是React Router?React Router提供了一种在React或React Native应用程序中实现路由的简便方法。入
React - Fiber原理
浏览器渲染 屏幕刷新率(FPS) 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿 绘制一次的称为一帧,平均每帧16.6ms 帧 每个帧的开头包括样式计算、布局和绘制 js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的 如果某个js任务执行时间过长,浏览器会推迟渲染,每