React解读--React架构篇

87 阅读9分钟

react15慢在哪里

React15之前的协调过程是同步的,也叫stack reconciler,同时因为js的执行是单线程的,这就导致了在更新比较耗时的任务时,不能及时响应一些高优先级的任务,比如用户的输入,所以页面就会卡顿,这就是cpu的限制,还有一个是IO限制

CPU限制

我们都知道,GUI渲染线程JS线程是互斥的。所以JS脚本执行浏览器布局、绘制不能同时执行。

在每16.6ms时间内,需要完成如下工作:

JS脚本执行 -----  样式布局 ----- 样式绘制

当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局样式绘制了,就会造成页面的卡顿,那么如何解决这个问题呢?

解决

试想一下,如果我们在日常的开发中,在单线程的环境中,遇到了比较耗时的代码计算会怎么做呢,首先我们可能会将任务分割,让它能够被中断,在其他任务到来的时候让出执行权,当其他任务执行后,再从之前中断的部分开始异步执行剩下的计算。所以关键是实现一套异步可中断的方案。

在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件,可以看到,在源码中,预留的初始时间是5ms,当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染UI,React则等待下一帧时间到来继续被中断的工作

开启ConCurrent Mode 启用时间切片

// 通过使用ReactDOM.unstable_createRoot开启Concurrent Mode
// ReactDOM.render(<App/>, rootEl);  
ReactDOM.unstable_createRoot(rootEl).render(<App/>);

此时我们的长任务被拆分到每一帧不同的task中,JS脚本执行时间大体在5ms左右,这样浏览器就有剩余时间执行样式布局样式绘制,减少掉帧的可能性。

没有开启ConCurrent Mode

image-20220107103908849

开启ConCurrent Mode

image-20220107103818125

Io瓶颈

当我们从一个页面进入另一个页面的时候,如果请求的时间过长,就会让用户觉得明显觉得很卡顿,所以,React让请求时间超过一个范围,那么会显示一个Loading效果。

解决

React实现了Suspense 功能及配套的hook——useDeferredValue ,而在源码内部,为了支持这些特性,同样需要将同步的更新变为可中断的异步更新

react三大概念

  • Fiber:react15的更新是同步的,因为它不能将任务分割,所以需要一套数据结构让它既能对应真实的dom又能作为分隔的单元,这就是Fiber。

  • Scheduler:有了Fiber,我们就需要用浏览器的时间片异步执行这些Fiber的工作单元,我们知道浏览器有一个api叫做requestIdleCallback,它可以在浏览器空闲的时候执行一些任务,我们用这个api执行react的更新,让高优先级的任务优先响应不就可以了吗,但事实是requestIdleCallback存在着浏览器的兼容性和触发不稳定的问题,所以我们需要用js实现一套时间片运行的机制,在react中这部分叫做scheduler

  • Lane:有了异步调度,我们还需要细粒度的管理各个任务的优先级,让高优先级的任务优先执行,各个Fiber工作单元还能比较优先级,相同优先级的任务可以一起更新,想想是不是更cool呢。

代数效应

除了cpu的瓶颈问题,还有一类问题是和副作用相关的问题,比如获取数据、文件操作等。不同设备性能和网络状况都不一样,react怎样去处理这些副作用,让我们在编码时最佳实践,运行应用时表现一致呢,这就需要react有分离副作用的能力,为什么要分离副作用呢,因为要解耦,这就是代数效应。

代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离。代数效应能够将副作用(比如请求图片数量)从函数逻辑中分离,使函数关注点保持纯粹

解耦副作用在函数式编程的实践中非常常见,例如redux-saga,将副作用从saga中分离,自己不处理副作用,只负责发起请求。

function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

严格上讲react不支持Algebraic Effects的,但react有Fiber啊,执行完Fiber的更新之后交还执行权给浏览器,让浏览器决定后面怎么调度,由此可见Fiber得是一个链表结构才能达到这样的效果,Suspense也是这种概念的延伸。

React 16架构

react的核心可以用ui=fn(state)来表示,更详细可以用

const state = reconcile(update);
const UI = commit(state);

上面的fn可以分为如下一个部分:

  • Scheduler(调度器): 排序优先级,让优先级高的任务先进行reconcile
  • Reconciler(协调器): 负责找出变化的组件,并打上不同的Flags(旧版本react叫Tag)
  • Renderer(渲染器): 将Reconciler中打好标签的节点渲染到视图上

相较于React15,React16中新增了Scheduler(调度器)

Scheduler (调度器)

其实部分浏览器已经实现了这个API,这就是requestIdleCallback 。但是由于以下因素,React放弃使用:

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低

基于以上原因,React实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置

Reconciler(协调器)

react-reconciler/src/ReactFiberWorkLoop.new.js line 1646

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间

React16是如何解决中断更新时DOM渲染不完全的问题?

ReconcilerRenderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样

react/packages/reactreconciler/src/ReactFiberFlags.js

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作

render阶段遍历Fiber树类似dfs的过程,捕获阶段发生在beginWork函数中,该函数做的主要工作是创建Fiber节点,计算state和diff算法,冒泡阶段发生在completeWork中,该函数主要是做一些收尾工作,例如处理节点的props、和形成一条effectList的链表,该链表是被标记了更新的节点形成的链表

Fiber

Fiber对象上面保存了包括这个节点的属性、类型、dom等,Fiber通过child、sibling、return(指向父节点)来形成Fiber树。

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

举个例子:

function App() {
  return (
    <div>
      你好啊
      <span>李银河</span>
    </div>
  )
}

对应的Fiber架构为:

image-20220107115926284

还保存了更新状态时用于计算state的updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

Fiber 双缓存

Q: 什么是双缓存?

双缓存是指存在两颗Fiber树,current Fiber树描述了当前呈现的dom树,workInProgress Fiber是正在更新的Fiber树,这两颗Fiber树都是在内存中运行的,在workInProgress Fiber构建完成之后会将它作为current Fiber应用到dom上。这种在内存中构建并直接替换的技术叫做双缓存

Fiber树与双缓存

current Fiber树中的Fiber节点被称为current fiberworkInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使current指针在不同Fiber树rootFiber间切换来完成current Fiber树指向的切换。即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

每次状态更新都会产生新的workInProgress Fiber树,通过currentworkInProgress的替换,完成DOM更新。

例如,我们吧上面的代码改成

function App() {
  return (
    <div>
      你好啊
      <span>YogLn</span>
    </div>
  )
}

对应的workInProgress Fibercurrent Fiber

image-20220107122307260

在mount时(首次渲染),会根据jsx对象(Class Component或的render函数者Function Component的返回值),构建Fiber对象,形成Fiber树,然后这颗Fiber树会作为current Fiber应用到真实dom上

在update(状态更新时如setState)的时候,会根据状态变更后的jsx对象和current Fiber做对比形成新的workInProgress Fiber,然后workInProgress Fiber切换成current Fiber应用到真实dom就达到了更新的目的,而这一切都是在内存中发生的,从而减少了对dom好性能的操作。