fiber
之前的render
- react15 传统的渲染流程:先创建父元素,然后循环递归创建子元素(render方法),这是没有fiber之前的渲染流程
- 这个方法的问题是:如果节点特别多,层级特别多,因为是递归调用,(栈调和),调用栈会特别深,递归的特点是不能暂停,一执行就执行到底。因为js是单线程,而且ui渲染和js执行是互斥的,如果递归需要的时间很长会阻塞渲染线程导致卡顿
图中的element是jsx,即虚拟节点树
- 为了解决这个问题,react团队决定把整个架构利用fiber全部重写
帧
- 大多数设备的屏幕刷新率是60次/秒,每帧大约16.66ms。每帧的开头包含样式计算/布局和绘制
- 当每秒绘制的帧数达到60,页面是流畅的,小于这个值时,用户会觉得卡顿
- requestAnimationFrame会在每帧布局绘制之前执行
Fiber
- fiber是一个执行单元。每次执行完一个执行单元,react会检查还剩多少时间,如果没有时间就将控制权交出去
- fiber也是一个数据结构。
- 要想把节点的渲染进行拆分,需要以一种结构来存储这些节点,也就是当执行完某个节点,需要暂停,怎么拿到下一个要执行的节点,这就需要一个明确的遍历顺序,永远知道下一个任务是谁。
- react目前的做法是使用链表,每个虚拟节点内部表示成一个fiber
- 链表通过child/sibling/return相互之间产生关联。节点之间:child指向第一个大儿子, sibling指向兄弟节点,每个节点的return指向自己的父亲节点
- 通过fiber可以把渲染过程分成一个个小任务,可以暂停(每个节点都是一个任务单元)
- 浏览器一般来说每帧会有10ms的空闲时间,可以利用这个空闲时间来进行渲染工作
requestAnimationFrame
- requestAnimationFrame回调函数会在绘制之前执行
- requestAnimationFrame(callback) 每次callback执行的时机都是浏览器刷新下一帧渲染周期的起点上
// 实现一个进度条
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div style="background-color:blue;width:0;height:20px"></div>
<button>开始</button>
<script>
let div = document.querySelector('div');
let button = document.querySelector('button');
let startTime;
function progress() {
div.style.width = div.offsetWidth + 1 + 'px';
div.innerHTML = div.offsetWidth + '%';
if(div.offsetWidth < 100) {
console.log(Date.now() - startTime + 'ms');
startTime = Date.now();
requestAnimationFrame(progress);
}
}
button.onclick = function() {
div.style.width = 0;
startTime = Date.now();
// 浏览器会在每一帧渲染前执行progress
requestAnimationFrame(progress);
}
</script>
</body>
</html>
requestIdleCallback
- 正常帧任务完成后没超过16ms,就会执行requestIdleCallback里注册的任务
- requestAnimationFrame的回调会在每一帧确定执行,requestIdleCallback则不一定,属于低优先级任务
MessageChannel
- 目前
requestIdleCallback
目前只有Chrome支持 - 所以目前 React利用 MessageChannel模拟了requestIdleCallback,将回调延迟到绘制操作之后执行
- MessageChannel API允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据
- MessageChannel创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据
- MessageChannel是一个宏任务
fiber执行阶段
- Reconciliation(render阶段) 和 Commit
- 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)
- 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断
render阶段
-
render阶段会构建fiber树
-
遍历规则
- 先儿子,后弟弟,再叔叔,辈份越小越优先
- 什么时候一个节点遍历完成? 没有子节点,或者所有子节点都遍历完成了
- 没爹了就表示全部遍历完成了
-
fiber其实也是一个普通的js对象
-
workLoop 开启工作循环
-
performUnitOfWork 执行当前的工作单元并返回下一个工作单元
-
nextUnitOfWork 是一个工作单元(js对象)
- stateNode 这个fiber对应的dom节点
- props fiber的属性
-
要将虚拟dom树 =〉 变成fiber树
-
执行一个工作单元 performUnitOfWork
- 先开始这个节点 beginWork(做了两件事情:1.创建真实dom,并没有挂载;2.创建fiber子树)
- 查看有没有子节点 如果有直接返回; 如果没有 则当前节点就完成了 completeUnitOfWork;
- 如果有弟弟 就返回弟弟节点; 如果没有,先指向父亲,又进入循环,父亲节点也完成,找父亲的兄弟节点
- beginWork
- 第一步: 创建真实dom
- 第二步: 循环当前节点的虚拟dom数组创建子fiber树
- completeUnitOfWork
- 构建副作用链 effectList 只有那些有副作用的节点
- 如何构建副作用链
- 把当前节点上的副作用链向上归并到父节点身上
- 看当前节点本身是否有副作用,如果有,挂到后面
commit阶段
- 整个循环结束之后 得到最终的副作用链 提交根节点 commitRoot