React 的设计理念是:React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。“快速响应“的制约因素有 CPU 和 网络:
为此 react 引入 Time Slicing 时间分片 和 Suspense。
React 采用 的 JSX,具有 JavaScript 的完整表现力,可以构建非常复杂的组件。但是灵活的语法,也意味着引擎难以理解,无法从模版层面进行静态分析,存在以上编译时先天不足,React 优化更多是运行时入手。
React 页面渲染存在以下两个阶段:
React15 及以前的架构:
- Reconciler(协调器)—— 负责找出变化的组件;采用递归的方式生成虚拟DOM,递归过程是不能中断的。
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上;
Stack Reconciler,若组件树的层级很深,递归更新占用线程耗时超过16ms,浏览器引擎会从多个函数的调用的执行栈的顶端开始执行,直到执行栈被清空才会停止。然后将执行权交还给浏览器。此时浏览器得不到控制权,事件响应和页面动画因为浏览器不能及时绘制下一帧,就会出现卡顿和延迟。
从 React 16 开始至今进行重构,目的是实现 Concurrent Mode(并发模式),即实现一套可中断/恢复的更新机制,由两部分组成:
React17 开始支持 concurrent mode,其根本目的是为了让应用保持cpu和io的快速响应,Concurrent Mode(并发模式)的功能包括 Fiber、Scheduler、Lane,可以根据用户硬件性能和网络状况调整应用的响应速度,核心就是为了实现异步可中断的更新。
首先,React16 将递归的无法中断的更新重构为异步的可中断更新——全新的Fiber架构应运而生,即 Stack Reconciler 重构为 Fiber Reconciler。
React16架构可以分为三层:
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler;
- Reconciler(协调器)—— 负责找出变化的组件:更新工作从递归变成了可以中断的循环过程。Reconciler内部采用了Fiber的架构;
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上。
Fiber Reconciler 中用链表遍历的方式替代了 React 16 之前的栈递归。
1. 使用多向链表替代树结构表示 DOM 结构
2. effect 单链表
3. state update 单链表
链表相比顺序结构(栈)的好处:
- 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了,插入和删除的时间复杂度是O(1)。
- 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到它的父节点或者兄弟节点。
但链表也不是完美的,缺点就是:
- 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。
- 不能自由读取,必须找到它的上一个节点,访问的时间复杂度是O(n)。
Fiber Reconciler 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在挂起和恢复过程中起到了关键作用。
然而,React16 的 expirationTimes 模型决定节点是否更新:>= expirationTimes。
React17 优化的 lanes模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。
Lane用二进制位表示任务的优先级,方便优先级的计算(位运算),不同优先级占用不同位置的“赛道”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的任务需要赋予什么优先级等问题都是需要Lane来解决。
原则上,1s 内绘制的帧数越多,画面越细腻,而浏览器一帧一般会经过以下几个过程:
而最后的 RIC 事件只有在每一帧预算(60Hz 对应为16.6ms)内前面的步骤执行完留有剩余才会执行。所以 RIC 执行时间过长就会影响控制权交还给浏览器去进行下一帧的渲染,导致页面出现卡顿和事件响应不及时。
因此,以浏览器是否有剩余时间作为任务中断的标准,那么就需要当浏览器有剩余时间时的通知机制。
由于原生 requestIdleCallback API 的兼容性以及触发频率不稳定的问题,React 实现了空闲时触发回调以及供任务设置多种调度优先级的 requestIdleCallbackpolyfill——Scheduler(调度器)。
在广义计算机科学概念中,Fiber 又是一种协作的(Cooperative)编程模型(协程),帮助开发者用一种【既模块化又协作化】的方式来编排代码。协程中子程序切换不是线程切换,是由一个线程执行的由程序(或用户)自身控制切换的。把一个耗时长的任务分成运行时间很短的很多小片,唯一的线程就不会被独占,所有任务都有运行的机会。
React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 中负责任务协调的模块,若无紧急任务则继续进行更新。
React Fiber 更新过程的可控主要体现在以下方面:
class FiberNode {
constructor(tag, pendingProps, key, mode) {
// 实例属性
this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件...
this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key ,也就是最终 ReactElement 上的
this.elementType = null; // createElement的第一个参数,ReactElement 上的 type
this.type = null; // 表示fiber的真实类型 ,elementType 基本一样,在使用了懒加载之类的功能时可能会不一样
this.stateNode = null; // 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是RootFiber,那么它上面挂的是 FiberRoot,如果是原生节点就是 dom 对象
// fiber
this.return = null; // 父节点,指向上一个 fiber
this.child = null; // 子节点,指向自身下面的第一个 fiber
this.sibling = null; // 兄弟组件, 指向一个兄弟节点
this.index = 0; // 一般如果没有兄弟节点的话是0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
this.ref = null; // reactElement 上的 ref 属性
this.pendingProps = pendingProps; // 新的 props
this.memoizedProps = null; // 旧的 props
this.updateQueue = null; // fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新
this.memoizedState = null; // 对应 memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系
this.mode = mode; // 表示当前组件下的子组件的渲染方式
// effects
this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新(更新、删除等)
this.nextEffect = null; // 指向下个需要更新的fiber
this.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个
this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个
this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成
this.childExpirationTime = NoWork; // child 过期时间
this.alternate = null; // current 树和 workInprogress 树之间的相互引用
}
}
function performUnitWork(currentFiber){
// beginWork(currentFiber) // 找到儿子,并通过链表的方式挂到currentFiber上,没有儿子就找后面那个兄弟
// 有儿子就返回儿子
if(currentFiber.child){
return currentFiber.child;
}
// 如果没有儿子,则找兄弟
while(currentFiber){
// completeUnitWork(currentFiber); // 将自己的副作用挂到父节点去
if(currentFiber.sibling){
return currentFiber.sibling
}
// 一直往上找
currentFiber = currentFiber.return;
}
}
component关系 对应的 fiber Node 关系:
workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链。
currentFiber 表示上次渲染构建的 Filber 树。在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用与 currentFiber 对应的节点对象,减少新创建对象带来的开销。
无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。更新过程就是根据输入数据以及现有的 fiber tree (workInProgress tree)构造出新的fiber tree(workInProgress tree)
其实最终都是为了收集到这条副作用链表,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的动作(commit)是不能中断,不然会造成视觉上的不连贯。
根据Fiber架构思想, isInputPending() API
在 Chromium 中被实现,
该 API 将中断的概念用于浏览器用户交互的的功能,并且允许 JavaScript 能够检查事件队列而不会将控制权交于浏览器。它可以提高网页的响应能力,但是不会对性能造成太大影响。
目前 React 实验版本允许用户选择三种 mode:
Concurrent Mode 下有两个最重要的特性在 React 18 已经很好支持了:
Pending -> Skeleton -> Complete
的更新路径,用户在切换页面时可以停留在当前页面,让页面保持响应。 相比展示一个无用的空白页面或者加载状态,这种用户体验更加友好。Concurrent mode(并发模式)也是未来 react 主要迭代的方向。再者,Concurrent mode 只是并发,更远的可能待解锁的能力是并行执行。