React原理理解

263 阅读10分钟

基础包结构

react

提供编写应用需要的api。

【使用场景】class组件中的setState(),function组件中的各种hook

react-dom

react渲染器,将react和web平台(浏览器、nodejs)连接起来,向web界面输出react-reconciler的运行结果,即表现fiber树,生成dom节点。

【使用场景】ReactDOM.render(<App/>, document.getElementByID('root'))引导react应用启动

scheduler

调度机制的核心实现,主要任务为执行回调,通过控制回调的执行时机,达到任务分片的目的,从而实现中断渲染

react-reconciler

协调其他三个包的配合与调用,管理react应用状态输入和结果输出,最终传递给渲染器。

内核关系

  • react-reconciler接收输入,将fiber树生成逻辑封装到一个回调函数中
  • 把上述回调函数送入scheduler接受调度
  • scheduler控制回调执行时机,执行完得到全新fiber
  • 最后调用渲染器react-dom,将fiber树形结构展示在界面上

image.png

工作循环区别

  • 任务调度循环的数据结构是二叉堆,循环执行堆定点直到清空;fiber构造循环的数据结构是树,深度优先遍历执行
  • 任务调度循环的逻辑是宏观的,负责调度任务task,不关心具体实现;fiber构造循环的逻辑是具体实现,是任务task的一部分

主要对象

fiber对象

一个fiber对象代表一个即将渲染或已经渲染的组件(ReactElement),一个组件可能对应两个fiber(current和WorkInProgress)

export type Fiber = {|
  tag: WorkTag,
  key: null | string,
  elementType: any,
  type: any,
  stateNode: any,
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  ref:
    | null
    | (((handle: mixed) => void) & { _stringRef: ?string, ... })
    | RefObject,
  pendingProps: any, // 从`ReactElement`对象传入的 props. 用于和`fiber.memoizedProps`比较可以得出属性是否变动
  memoizedProps: any, // 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中
  updateQueue: mixed, // 存储state更新的队列, 当前节点的state改动之后, 都会创建一个update对象添加到这个队列中.
  memoizedState: any, // 用于输出的state, 最终渲染所使用的state
  dependencies: Dependencies | null, // 该fiber节点所依赖的(contexts, events)等
  mode: TypeOfMode, // 二进制位Bitfield,继承至父节点,影响本fiber节点及其子树中所有节点. 与react应用的运行模式有关(有ConcurrentMode, BlockingMode, NoMode等选项).


  // Effect 副作用相关
  flags: Flags, // 标志位
  subtreeFlags: Flags, //替代16.x版本中的 firstEffect, nextEffect. 当设置了 enableNewReconciler=true才会启用
  deletions: Array<Fiber> | null, // 存储将要被删除的子节点. 当设置了 enableNewReconciler=true才会启用


  nextEffect: Fiber | null, // 单向链表, 指向下一个有副作用的fiber节点
  firstEffect: Fiber | null, // 指向副作用链表中的第一个fiber节点
  lastEffect: Fiber | null, // 指向副作用链表中的最后一个fiber节点


  // 优先级相关
  lanes: Lanes, // 本fiber节点的优先级
  childLanes: Lanes, // 子节点的优先级
  alternate: Fiber | null, // 指向内存中的另一个fiber, 每个被更新过fiber节点在内存中都是成对出现(current和workInProgress)


  // 性能统计相关(开启enableProfilerTimer后才会统计)
  // react-dev-tool会根据这些时间统计来评估性能
  actualDuration?: number, // 本次更新过程, 本节点以及子树所消耗的总时间
  actualStartTime?: number, // 标记本fiber节点开始构建的时间
  selfBaseDuration?: number, // 用于最近一次生成本fiber节点所消耗的时间
  treeBaseDuration?: number, // 生成子树所消耗的时间的总和
  • fiber.tag: 表示 fiber 类型, 根据ReactElement组件的 type 进行生成, 在 react 内部共定义了25 种 tag.
  • fiber.key: 和ReactElement组件的 key 一致.
  • fiber.elementType: 一般来讲和ReactElement组件的 type 一致
  • fiber.type: 一般来讲和fiber.elementType一致. 一些特殊情形下, 比如在开发环境下为了兼容热更新(HotReloading), 会对function, class, ForwardRef类型的ReactElement做一定的处理, 这种情况会区别于fiber.elementType, 具体赋值关系可以查看源文件.
  • fiber.stateNode: 与fiber关联的局部状态节点(比如: HostComponent类型指向与fiber节点对应的 dom 节点; 根节点fiber.stateNode指向的是FiberRoot; class 类型节点其stateNode指向的是 class 实例).
  • fiber.return: 指向父节点.
  • fiber.child: 指向第一个子节点.
  • fiber.sibling: 指向下一个兄弟节点.
  • fiber.index: fiber 在兄弟节点中的索引, 如果是单节点默认为 0.
  • fiber.ref: 指向在ReactElement组件上设置的 ref(string类型的ref除外, 这种类型的ref已经不推荐使用, reconciler阶段会将string类型的ref转换成一个function类型).
  • fiber.pendingProps: 输入属性, 从ReactElement对象传入的 props. 用于和fiber.memoizedProps比较可以得出属性是否变动.
  • fiber.memoizedProps: 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中. 向下生成子节点之前叫做pendingProps, 生成子节点之后会把pendingProps赋值给memoizedProps用于下一次比较.pendingPropsmemoizedProps比较可以得出属性是否变动.
  • fiber.updateQueue: 存储update更新对象的队列, 每一次发起更新, 都需要在该队列上创建一个update对象.
  • fiber.memoizedState: 上一次生成子节点之后保持在内存中的局部状态.
  • fiber.dependencies: 该 fiber 节点所依赖的(contexts, events)等, 在context机制章节详细说明.
  • fiber.mode: 二进制位 Bitfield,继承至父节点,影响本 fiber 节点及其子树中所有节点. 与 react 应用的运行模式有关(有 ConcurrentMode, BlockingMode, NoMode 等选项). 【Effect副作用相关】
  • fiber.flags: 标志位, 副作用标记(在 16.x 版本中叫做effectTag, 相应pr), 在ReactFiberFlags.js中定义了所有的标志位. reconciler阶段会将所有拥有flags标记的节点添加到副作用链表中, 等待 commit 阶段的处理.
  • fiber.subtreeFlags: 替代 16.x 版本中的 firstEffect, nextEffect. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.
  • fiber.deletions: 存储将要被删除的子节点. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.
  • fiber.nextEffect: 单向链表, 指向下一个有副作用的 fiber 节点.
  • fiber.firstEffect: 指向副作用链表中的第一个 fiber 节点.
  • fiber.lastEffect: 指向副作用链表中的最后一个 fiber 节点. 【优先级相关】
  • fiber.lanes: 本 fiber 节点所属的优先级, 创建 fiber 的时候设置.
  • fiber.childLanes: 子节点所属的优先级.
  • fiber.alternate: 指向内存中的另一个 fiber, 每个被更新过 fiber 节点在内存中都是成对出现(current 和 workInProgress)

fiber与ReactElement对照关系

image.png image.png

Hook对象

export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
|};


type Update<S, A> = {|
  lane: Lane,
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null,
  next: Update<S, A>,
  priority?: ReactPriorityLevel,
|};


type UpdateQueue<S, A> = {|
  pending: Update<S, A> | null,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
|};
  • memoizedState: 内存状态, 用于输出成最终的fiber
  • baseState: 基础状态, 当Hook.queue更新过后, baseState也会更新.
  • baseQueue: 基础状态队列, 在reconciler阶段会辅助状态合并.
  • queue: 指向一个Update队列
  • next: 指向该function组件的下一个Hook对象, 使得多个Hook之间也构成了一个链表.

优先级管理

分类

fiber优先级(LanePriority)

属于react-reconciler包,使用于与fiber构造过程相关的方法(fiber.updateQueue, fiber.lanes),定义于ReactFiberLane.js见源码

调度优先级(SchedulerPriority)

属于scheduler包,定义于SchedulerPriorities.js中(见源码

优先级等级(ReactPriority)

属于react-reconciler包,定义于SchedulerWithReactIntegration.js中(见源码LanePrioritySchedulerPriority通过ReactPriorityLevel进行转换

调度原理

内核重要函数

shouldYieldToHost(是否让出主线程)

判定条件:

  • currentTime >= deadline: 只有时间超过deadline之后才会让出主线程(其中deadline = currentTime + yieldInterval).
    • yieldInterval默认是5ms, 只能通过forceFrameRate函数来修改(事实上在 v17.0.2 源码中, 并没有使用到该函数).
    • 如果一个task运行时间超过5ms, 下一个task执行之前, 会把控制权归还浏览器.
  • navigator.scheduling.isInputPending(): 这 facebook 官方贡献给 Chromium 的 api, 现在已经列入 W3C 标准(具体解释), 用于判断是否有输入事件(包括: input 框输入事件, 点击事件等)

任务队列管理

scheduler中维护了一个任务队列taskQueue和延时任务队列timerQueue

创建任务

  1. 获取当前时间
var currentTime = getCurrentTime();
  1. 根据传入的优先级,设置任务过期时间expirationTime
switch (priorityLevel) {
    ...
}
var expirationTime = startTime + timeout;
  1. 创建新任务
var newTask = {
  id: taskIdCounter++, // id: 一个自增编号
  callback, // callback: 传入的回调函数
  priorityLevel, // priorityLevel: 优先级等级
  startTime, // startTime: 创建task时的当前时间
  expirationTime, // expirationTime: task的过期时间, 优先级越高 expirationTime = startTime + timeout 越小
  sortIndex: -1,
};
  1. 加入任务队列
push(taskQueue, newTask);
  1. 请求调度
requestHostCallback(flushWork);

消费任务

创建任务的第5步请求调度传入调度中心内核的flushWork回调函数,其中会调用workLoop,其包含了队列消费的主要逻辑,也就是任务调度循环

每一次while循环的退出就是一个时间切片, 深入分析while循环的退出条件:

  1. 队列被完全清空: 这种情况就是很正常的情况, 一气呵成, 没有遇到任何阻碍.
  2. 执行超时: 在消费taskQueue时, 在执行task.callback之前, 都会检测是否超时, 所以超时检测是以task为单位.
    • 如果某个task.callback执行时间太长(如: fiber树很大, 或逻辑很重)也会造成超时
    • 所以在执行task.callback过程中, 也需要一种机制检测是否超时, 如果超时了就立刻暂停task.callback的执行.

时间切片原理

消费任务队列的过程中,可以消费1-n个task。但每次执行task.callback前都要超时检测,如果超时可以立刻退出循环并等待下次调用

可中断渲染原理

在时间切片的基础之上,在fiber树构造过程中,没构造完成一个单元,都会做超时检测,如果单个task.callback执行时间很长就退出循环,并返回一个新的回调函数(continuationCallback)并等待下次回调继续未完成的fiber树构造

fiber树构造

基础准备

ReactElement对象 -> fiber对象 -> DOM对象

双缓冲技术

ReactElement转换为fiber树的过程中,内存中会同时存在2棵fiber树:

  • 代表当前界面的树:已展示,挂载在fiberRoot.current,初始化渲染时该fiber树为null
  • 正在构造的fiber树:即将展示,挂载在HostRootFiber.alternate上,正在构造的节点成为workInProgress,构造完成后切换fiberRoot.current = workInProgress

优先级

update优先级

update对象中的lane属性代表其优先级,根据当前时间创建 两种情况下会创建update对象:

  1. 应用初始化:react-reconciler包中updateContainer函数执行
  2. 发起组件更新:class组件中调用setState
  • 当在legacy或blocking模式中,如果执行上下文为空,会取消schedule调度,主动刷新回调,立即进入fiber树构造过程,当执行setState下一行代码时,fiber树已经重新渲染了,则setState表现为同步
  • 正常情况下不会取消schedule调度,因为其由宏任务MessageChannel触发,则setState表现为异步

渲染优先级

fiber优先级

  • fiber.lanes:本节点优先级
  • fiber.childLanes:子节点优先级 初始值均为NoLanes,fiber树构造过程中,使用全局的渲染优先级renderLanesfiber.lanes判断fiber节点是否更新
  • 如果renderLanes不包括fiber.lanes,证明该fiber节点没有更新,可以复用
  • 如果不能复用,则进入创建阶段

栈帧管理

全局变量记录了fiber树构造的活动,因此可通过这些变量还原其构造过程,如时间切片过程,全局变量用于在构造被打断之后还原进度。每次fiber树构造都是独立的过程,需要独立的一组全局变量,React内部将这个过程封装为一个栈帧

初次创建

在深度优先遍历构建中,每个fiber节点都会经历2个阶段

探寻阶段beginWork

  1. 根据ReactElement对象创建所有fiber节点
  2. 设置二进制变量fiber.flags,用于标记fiber节点增删改状态
  3. 设置fiber.stateNode局部状态,如Class类型

回溯阶段completeWork

  1. 给fiber节点创建DOM实例,设置fiber.stateNode局部状态
  2. 给DOM节点设置属性
【参考资料】

图解React 7kms.github.io/react-illus…