一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情
哈喽,大家好 我是
xy
👨🏻💻。前端面试经常会遇到面试官拷问源码方面的知识,比如:虚拟DOM
,Diff算法
,Fiber
等... 这不前段时间就有个学弟在面试
的时候就被问住了。
面试官问:
- “
React Fiber
是什么?” - 为什么叫 “
Fiber
”? Fiber
架构解决了什么问题?
怎么回答
遇到这样的问题,一般从以下几个方面
去回答:
- 说下
react16
之前stack架构
,递归遍历
组件树成本很高,会造成主线程被持续占⽤,结果就是主线程上的布局、动画等周期性任务就⽆法⽴即得到处理,造成视觉上的卡顿
,影响⽤户体验
Fiber架构
任务分解,避免主线程的持续占用造成卡顿问题增量渲染
,把渲染任务拆分成多块- 更新时候能够
暂停
,终止
,复用
渲染任务 - 给
不同类型
的更新赋予优先级
基本上按照上面的几个方面回答,都不会有太大的问题
那么有很多同学会问,react 中是否还有虚拟Dom
,diff算法
呢?
其实这里可以简单的理解为:
📚
虚拟dom
react 中叫Fiber
,diff算法
react 中叫协调
为什么会出现 React fiber 架构
React 15 Stack Reconciler
是通过递归
更新子组件 。由于递归执行,所以更新一旦开始
,中途就无法中断
。当层级很深时,递归更新时间超过了 16ms
,用户交互就会卡顿。
Stack reconciler
的工作流程很像函数的调用过程
。父组件里调子组件,可以类比为函数的递归
(这也是为什么被称为stack reconciler
的原因)。
在setState
后,react 会立即开始reconciliation
过程,从父节点
(Virtual DOM)开始遍历
,以找出不同。将所有的Virtual DOM遍历完
成后,reconciler才
能给出当前需要修改真实DOM的信息
,并传递给renderer
,进行渲染,然后屏幕上才会显示此次更新
内容。
对于特别庞大的DOM树
来说,reconciliation过程会很长(x00ms)
,在这期间,主线程是被 js 占用的,因此任何交互、布局、渲染都会停止
,给用户的感觉就是页面被卡住了。
网友测试使用React V15
,当DOM节点
数量达到100000
时, 加载页面时间竟然要 7 秒;
当然以上极端情况一般不会出现,官方为了解决这种特殊情况。在 Fiber 架构中使用了Fiber reconciler
。
React Fiber 是什么?
官方的一句话解释是“React Fiber是对核心算法的一次重新实现
”。Fiber 架构调整很早就官宣了,但官方经过两年时间才在V16版本
正式发布。官方概念解释太笼统, 其实简单来说 React Fiber
是一个新的任务调和器
(Reconciliation
)简称协调
简单理解就是把一个耗时长的任务
分解为一个个的工作单元(每个工作单元运行时间很短,不过总时间依然很长)。在执行工作单元之前,由浏览器判断是否有空余时间执行
,有时间就执行
工作单元,执行完成后,继续判断是否还有空闲时间。没有时间就终止执行
让浏览器执行其他任务(如 GUI 线程等)。等到下一帧执行时判断是否有空余时间,有时间就从终止的地方继续执行工作单元,一直重复
到任务结束。
Fiber架构
=Fiber节点
+Fiber调度算法
要让终止的任务恢复执行
,就必须知道下一工作单元
对应那一个。所以要实现工作单元的连接,就要使用链表
,在每个工作单元中保存下一个工作单元的指针
,就能恢复任务的执行。
要知道每一帧的空闲时间,就需要使用 requestIdleCallback
Api。传入回调函数,回调函数接收一个参数(剩余时间),如果有剩余时间,那么就执行工作单元,如果时间不足了,则继续requestIdleCallback
,等到下一帧继续判断。
📚
所以 Fiber 架构就是用 异步的方式解决旧版本 同步递归导致的性能问题
。
如果还不知道什么是链表
的同学,可以看下我之前的文章:
为什么叫 “Fiber”?
大家应该都清楚进程(Process)和线程(Thread)的概念,进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元,在计算机科学中还有一个概念叫做 Fiber
,英文含义就是“纤维
”,意指比 Thread 更细的线,也就是比线程(Thread)控制得更精密的并发处理机制。
上面说的 Fiber
和 React Fiber
不是相同的概念,但是,React 团队把这个功能命名为 Fiber,含义也是更加紧密的处理机制
,比 Thread 更细。
Fiber 架构解决了什么问题?
Reconciliation
React 官方核心算法名称是 Reconciliation
, 中文翻译是“协调
”!React diff 算法的实现 就与之相关。
先简单回顾下 React Diff: React 首创了“虚拟 DOM”概念, “虚拟 DOM”能火并流行起来主要原因在于该概念对前端性能优化的突破性创新
稍微了解浏览器加载页面原理的前端同学都知道网页性能问题大都出现在 DOM 节点频繁操作上
而 React 通过“虚拟 DOM
” + React Diff
算法保证了前端性能
;
传统 Diff 算法
通过循环递归
对节点进行依次对比,算法复杂度达到 O(n^3) ,n 是树的节点数,这个有多可怕呢?
如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异
React Diff 算法
将 Virtual DOM
(虚拟 Dom)树转换成 Actual DOM
(真实 Dom)树的最少操作
的过程,叫作协调
(Reconciliaton)。diff 算法是协调的具体实现
,将 O(n^3)复杂度 转化为 O(n)复杂度。
diff 算法原则:
- 分层同级比较,不跨层比较;
- 相同的组件生成的 DOM 结构类似;
- 分组内的同级节点通过唯一的 id 进行区分(key)
不同类型节点比较:
- 逐层比较,不同类型节点直接替换,组件经历 unmount,mount
同类型,不同属性节点比较:
同类型节点一般会出现以下几种形式更新:
- 插入新的同级节点
- 删除同级节点
- 同级节点交换位置
- 更新节点属性
React 中的 React fiber 架构
- 使用
Fiber
节点, 来代替虚拟 DOM
原来的结构。
// 链表结构
export type Fiber = {
// Fiber 类型信息
type: any,
// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
stateNode: any,
...
// 指向父节点,或者render该节点的组件
return: Fiber | null,
// 指向第一个子节点
child: Fiber | null,
// 指向下一个兄弟节点
sibling: Fiber | null,
}
- 通过
ReactDOM.render()
和setState
把待更新的任务
会先放入队列中
, 然后通过requestIdleCallback
请求浏览器调度。
// 更新节点 放入数组中
updateQueue.push(updateTask);
requestIdleCallback(performWork, {timeout});
- 现在浏览器
有空闲
或者超时
了就会调用performWork
来执行任务:
// performWork 会拿到一个Deadline,表示剩余时间
function performWork(deadline) {
// 循环取出updateQueue中的任务
while (updateQueue.length > 0 && deadline.timeRemaining() > ENOUGH_TIME) {
workLoop(deadline);//
}
// 如果在本次执行中,未能将所有任务执行完毕,那就再请求浏览器调度
if (updateQueue.length > 0) {
requestIdleCallback(performWork);
}
}
- 这里的
nextUnitOfWork
下一个工作单元是Fiber
结构,所以终止了之后也能恢复继续执行
。
// 保存当前的处理现场
let nextUnitOfWork: Fiber | undefined // 保存下一个需要处理的工作单元
let topWork: Fiber | undefined // 保存第一个工作单元
function workLoop(deadline: IdleDeadline) {
// updateQueue中获取下一个或者恢复上一次中断的执行单元
if (nextUnitOfWork == null) {
nextUnitOfWork = topWork = getNextUnitOfWork();
}
// 每执行完一个执行单元,检查一次剩余时间
// 如果被中断,下一次执行还是从 nextUnitOfWork 开始处理
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
// 处理节点 并 返回下一个 要处理得节点
nextUnitOfWork = performUnitOfWork(nextUnitOfWork, topWork);
}
// 提交工作,当任务全部执行完后 一次全部更新 同步执行
if (pendingCommit) {
// commit 阶段
commitAllWork(pendingCommit);
}
}
/**
* 返回下一个 要处理的 nextUnitOfWork
* @params fiber 当前需要处理的节点
* @params topWork 本次更新的根节点
*/
function performUnitOfWork(fiber: Fiber, topWork: Fiber) {
// 对该节点进行处理
// diff算法 为修改的节点打上标签
// 在fiber上 生成对应的stateNode (真实的DOM节点)
beginWork(fiber);
// 如果存在子节点,那么下一个待处理的就是子节点
if (fiber.child) {
return fiber.child;
}
// 没有子节点了,上溯查找兄弟节点
let temp = fiber;
while (temp) {
completeWork(temp);// 收集副作用函数 commit 阶段执行
// 到顶层节点了, 退出
if (temp === topWork) {
break
}
// 找到,下一个要处理的就是兄弟节点
if (temp.sibling) {
return temp.sibling;
}
// 没有, 继续上溯
temp = temp.return;
}
}
- 渲染阶段, 协调阶段完成后生成了
WorkInProgress Tree
,在有修改的Fiber
节点中都有一个标签,在Renderer
阶段循环WorkInProgress Tree
进行修改节点
然后渲染
到页面上。
// 任务都执行完后 进入commit 修改真实Tree
function commitAllWork(fiber) {
if(!fiber) {
return;
}
const parentDom = fiber.return.dom;
if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) {
parentDom.appendChild(fiber.dom);
} else if(fiber.effectTag === 'DELETION') {
parentDom.removeChild(fiber.dom);
} else if(fiber.effectTag === 'UPDATE' && fiber.dom) {
// 更新DOM属性
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
// 递归操作子元素和兄弟元素
commitRootImpl(fiber.child);
commitRootImpl(fiber.sibling);
}
写在最后
大家好,我是一名前端🤫爱好:瞎折腾
如果你也是一名瞎折腾的前端欢迎加我微信交流哦...