前言
Fiber树
- 从上一篇可以发现,解决浏览器卡顿其实是把一个大任务拆成多个小任务。而react里,diff操作可以算是一个非常大的任务,所以需要将这个大任务拆成多个小任务解决。
- 那么如何拆呢?就是通过把一个递归操作,变成一个可中断操作。
- 和上一篇原理一样,每次完成一个小任务,检查时间够不够执行下一个任务,不够的话将控制权交给浏览器,等待浏览器渲染完继续下一个任务。
- fiber的结构很简单,指向父节点的return,指向孩子的child,指向兄弟的sibiling。但这个结构特殊在于父节点没法一次头拿齐所有的孩子,必须通过链表找到所有孩子。
- 这里举个例子,看一下如何拆成多个小任务的:
- 比如有这样一个结构关系,传统的递归是这样做:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/67916c61e6b2aa12095e03c4fd467216.jpeg)
let C1 = { type: 'div', key: 'C1'};
let C2 = { type: 'div', key: 'C2'};
let B1 = { type: 'div', key: 'B1', child:[C1,C2] };
let B2 = { type: 'div', key: 'B2'};
let A1 = { type: 'div', key: 'A1', child:[B1,B2] };
function deep(root){
console.log(root.key)
if(root.child){
root.child.forEach(it => {
deep(it)
});
}
}
deep(A1)
A1
B1
C1
C2
B2
- 这个过程一气呵成无法中断。
- 我们将其结构改为fiber,就是这样:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/08f7629073a4331218a3137f54563f47.jpeg)
- 制作出相应的结构:
let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 };
let B2 = { type: 'div', key: 'B2', return: A1 };
let C1 = { type: 'div', key: 'C1', return: B1 };
let C2 = { type: 'div', key: 'C2', return: B1 };
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2
let nextUnitOfwork=A1
function workLoop(){
while (nextUnitOfwork){
nextUnitOfwork=performUnitOfWork(nextUnitOfwork)
}
}
function performUnitOfWork(fiber){
console.log('start',fiber.key)
if(fiber.child){
return fiber.child
}
while(fiber){
console.log('complete',fiber.key)
if(fiber.sibling){
return fiber.sibling
}
fiber=fiber.return
}
}
workLoop()
start A1
start B1
start C1
complete C1
start C2
complete C2
complete B1
start B2
complete B2
complete A1
- 这里当fiber进入while,便会去找他的兄弟或者父亲,这个节点就算遍历完成状态。当然如果返回父亲,父亲也会在while循环里,所以不是找兄弟就是找爷爷。以此类推。
- 这样就把一个传统递归变成了一个fiber形式递归。然后还需要拆成小任务:
let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 };
let B2 = { type: 'div', key: 'B2', return: A1 };
let C1 = { type: 'div', key: 'C1', return: B1 };
let C2 = { type: 'div', key: 'C2', return: B1 };
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2
let nextUnitOfwork = A1
function workLoop(deadline) {
while ((deadline.timeRemaining() > 0 || deadline.timeout) && nextUnitOfwork) {
nextUnitOfwork = performUnitOfWork(nextUnitOfwork)
}
if (!nextUnitOfwork) {
console.log('over')
} else {
window.requestIdleCallback(workLoop, { timeout: 1000 })
}
}
function performUnitOfWork(fiber) {
console.log('start', fiber.key)
if (fiber.child) {
return fiber.child
}
while (fiber) {
console.log('complete', fiber.key)
if (fiber.sibling) {
return fiber.sibling
}
fiber = fiber.return
}
}
window.requestIdleCallback(workLoop, { timeout: 1000 })
- 同步部分改个判断,当时间不够交给浏览器进行递归。
- 当然目前这个任务耗时比较短,我们可以在performUnitOfWork里穿插别的逻辑比如上篇的sleep,让其在某个fiber中运行的耗时多一点,可以试一下。
if (fiber.key === 'B1') {
sleep(2000)
}
- 有人可能觉得,这效果用generator也能做啊,为啥这么搞?
- 这个问题可以参考issue。里面说了generator存在2个问题,其中最大的问题就是没法重用已经算好的值。
副作用链表
- fiber分为2个阶段,一个叫reconciliation阶段,一个叫commit阶段。而上面那段搞得就是reconciliation阶段,这段说的副作用链表也是reconciliation阶段产物。
- 上面那段做的这个fiber树中,会在console complete那个地方进行副作用收集。
- 收集的其实就是副作用,比如dom的挂载卸载更新之类。
- 收集完就得到一个副作用链表。
- 到这里reconciliation阶段就搞完了,reconciliation阶段是可以中断的,中断原理就是上面部分所讲。而commit阶段则是将这个副作用链表上所有操作搞一遍,期间不能中断,否则一个元素一个元素渲染不是看起来很怪异?特别需求另外说。
- 链表就是有指针指向下一个节点的嘛,所以,这个副作用链表在递归过程中,通过fiber树结构中的指向,来获取上一个有副作用的节点,将其指向下一个有副作用的节点,从而完整的形成一个链表。
- 基本基础就这些内容,剩下的下次写。