这是一篇讲react Fiber算法的文章,深入浅出,并且作者自己实现了Fiber的核心代码,可以很好的帮助我们理解fiber
原文链接
另外,建议读这篇文章之前先看一下他的另外几篇关于react的文章,本篇是建立在其之上的
DIY React
Didact Fiber: Incremental reconciliation
Why Fiber
本文并不会展示一个完整的React Fiber,如果你想了解更多,更多资料
当浏览器的主线程长时间忙于运行一些事情时,关键任务的执行可以能被推迟。
为了展示这个问题,我做了一个demo,为了使星球一直转动,主线程需要每16ms被调用一次,因为animation是跑在主线程上的。如果主线程被其他事情占用,假如占用了200ms,你会发现animation会发生卡顿,星球停止运行,直到主线程空闲出来运行animation。
到底是什么导致主线程如此繁忙导致不能空闲出几微秒去保持动画流畅和响应及时呢?
还记得以前实现的reconciliation code吗?一旦开始,就无法停止。如果此时主线程需要做些别的事情,那就只能等待。并且因为使用了许多递归,导致很难暂停。这就是为什么我们重写代码,用循环代替递归。
Scheduling micro-tasks
我们需要把任务分成一个个子任务,在很短的时间里运行结束掉。可以让主线程先去做优先级更高的任务,然后再回来做优先级低的任务。
我们将会需要requestIdleCallback()函数的帮助。它在浏览器空闲时才执行callback函数,回调函数中deadline
参数会告诉你还有多少空闲时间来运行代码,如果剩余时间不够,那么你可以选择不执行代码,保持了主线程不会被一直占用。
1 | const ENOUGH_TIME = 1; // milliseconds |
真正起作用的函数是performUnitOfWork。我们将会在其中写reconciliation code。函数运行一次占用很少的时间,并且返回下一次任务的信息。
为了组织这些子任务,我们将会使用fibers
The fiber data structure
我们将会为每一个需要渲染的组件创建一个fiber。nextUnitOfWork
是对将要运行的下一个fiber的引用。performUnitOfWork
会对fiber进行diff,然后返回下一个fiber。这个将会在后面详细解释。
fiber是啥样子的呢?1
2
3
4
5
6
7
8
9
10
11
12
13let fiber = {
tag: HOST_COMPONENT,
type: "div",
parent: parentFiber,
child: childFiber,
sibling: null,
alternate: currentFiber,
stateNode: document.createElement("div"),
props: { children: [], className: "foo"},
partialState: null,
effectTag: PLACEMENT,
effects: []
};
是一个对象啊,我们将会使用parent,child,sibling属性去构建fiber树来表示组件的结构树。
stateNode
是对组件实例的引用。他可能是DOM元素或者用户定义的类组件实例
举个例子:
在上面例子中我们可以看到将支持三种不同的组件:
- b, p, i 代表着
host component
。我们将会用tag:HOST_COMPONENT来定义他。type属性将会是字符串。props是dom属性和事件。 - Foo class component。它的tag:CLASS_COMPONENT, type指向用户定义的类组件
- div代表着 host root。他类似于host component,stateNode也是DOM element.tag: HOST_ROOT.注意stateNode就是传递给render函数的参数。
另外一个重要属性就是alternate
,我们需要它是因为大多数时间我们将会有两个fiber tree。一个代表着已经渲染的dom, 我们成其为current tree 或者 old tree。另外一个是在更新(当调用setState或者render)时创建的,称其为work-in-progress tree。
work-in-progress tree不会与old tree共享任何fiber。一旦我们完成work-in-progress tree的构建和dom的改变,work-in-progress tree就变成了old tree。
所以我们使用alternate属性去链接old tree。fiber与其alternate有相同的tag,type,statenode。有时我们渲染新的组件,它可能没有alternate属性
然后,还有一个effects 列表和effectTag。当我们发现work-in-progress需要改变的DOM时,就将effectTag
设置为PLACEMENT
, UPDATE
, DELETION
。为了更容易知道总共有哪些需要fiber需要改变DOM,我们把所有的fiber放在effects列表里。
可能这里讲了许多概念的东西,不要担心,我们将会用行动来展示fiber。
Didact call hierarchy
为了对程序有整体的理解,我们先看一下结构示意图
我们将会从render()
和setState()
开始,在commitAllWork()结束
Old code
我之前告诉你我们将重构大部分代码,但在这之前,我们先回顾一下不需要重构的代码
这里我就不一一翻译了,这些代码都是在文章开头我提到的
- Element creation and JSX
- Instances, reconciliation and virtual DOM
- Components and state,这里的 Class Component需要稍微改动一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Component {
constructor(props) {
this.props = props || {};
this.state = this.state || {};
}
setState(partialState) {
scheduleUpdate(this, partialState);
}
}
function createInstance(fiber) {
const instance = new fiber.type(fiber.props);
instance.__fiber = fiber;
return instance;
}
render() & scheduleUpdate()
除了Component
, createElement
, 我们将会有两个公共函数render()
, setState()
,我们已经看到setState()
仅仅调用了scheduleUpdate()
。
render()
和 scheduleUpdate()
非常类似,他们接收新的更新并且进入队列。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/ Fiber tags
const HOST_COMPONENT = "host";
const CLASS_COMPONENT = "class";
const HOST_ROOT = "root";
// Global state
const updateQueue = [];
let nextUnitOfWork = null;
let pendingCommit = null;
function render(elements, containerDom) {
updateQueue.push({
from: HOST_ROOT,
dom: containerDom,
newProps: { children: elements }
});
requestIdleCallback(performWork);
}
function scheduleUpdate(instance, partialState) {
updateQueue.push({
from: CLASS_COMPONENT,
instance: instance,
partialState: partialState
});
requestIdleCallback(performWork);
}
我们将会使用updateQueue
数组来存储等待的更新。每一次调用render
或者 scheduleUpdate
都会将数据存储进updateQueue
。数组里每一个数据都不一样,我们将会在resetNextUnitOfWork()
函数中使用。
在将数据push
存储进队列之后,我们将会异步调用performWork()
。
performWork() && workLoop()
1 | const ENOUGH_TIME = 1; // milliseconds |
这里使用了我们之前看到的performUnitOfWork
模式。
workLoop()
中判断deadline是不是有足够的时间来运行代码,如果不够,停止循环,回到performWork()
,并且nextUnitOfWork
还被保留为下次任务,在performWork()
中判断是否还需要执行。
performUnitOfWork()
的作用是构建 work-in-progress tree和找到哪些需要操作DOM的改变。这种处理方式是递增的,一次只处理一个fiber。
如果performUnitOfWork()
完成了本次更新的所有工作,则renturn值为null,并且调用commitAllWork
改变DOM。
至今为止,我们还没有看到第一个nextUnitOfWork
是如何产生的
resetUnitOfWork()
函数取出updateQueue
第一项,将其转换成nextUnitOfWork
.
1 | function resetNextUnitOfWork() { |
如果update包含partialState
, 就将其保存的对应fiber上,在后面会赋值给组件实例,已供render使用。
然后,我们找到old fiber树的根节点。如果update是first render调用的,root fiber将为null。如果是之后的render,root将等于_rootContainerFiber
。如果update是因为setState()
,则向上找到第一个没有patient属性的fiber。
然后我们将其赋值给nextUnitOfWork
,注意,这个fiber将会是work-in-progress的根元素。
如果没有old root。stateNode将取render()中的参数。props将会是render()的另外一个参数。props中children是数组。alternate
是 null。
如果有old root。stateNode是之前的root DOM node。props将会是newProps,如果其值不为null的话,否则就是原来的props。alternate
就是之前的old root。
我们现在已经有了work-in-progress的根元素,让我们构造剩下的吧
performUnitOfWork()
1 | function performUnitOfWork(wipFiber) { |
performUnitOfWork()
遍历work-in-progress树
beginWork()
的作用是创建子节点的fiber。并且将第一次子节点作为fiber的child属性
如果当前fiber没有子节点,我们就调用completeWork()
,并且返回sibling
作为下一个nextUnitOfWork
.
如果没有sibling
,就继续向上操作parent fiber。直到root。
总的来说,就是先处理叶子节点,然后是其兄弟节点,然后是双亲节点。从下往上遍历。
beginWork(), updateHostComponent(), updateClassComponent()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34unction beginWork(wipFiber) {
if (wipFiber.tag == CLASS_COMPONENT) {
updateClassComponent(wipFiber);
} else {
updateHostComponent(wipFiber);
}
}
function updateHostComponent(wipFiber) {
if (!wipFiber.stateNode) {
wipFiber.stateNode = createDomElement(wipFiber);
}
const newChildElements = wipFiber.props.children;
reconcileChildrenArray(wipFiber, newChildElements);
}
function updateClassComponent(wipFiber) {
let instance = wipFiber.stateNode;
if (instance == null) {
// Call class constructor
instance = wipFiber.stateNode = createInstance(wipFiber);
} else if (wipFiber.props == instance.props && !wipFiber.partialState) {
// No need to render, clone children from last time
cloneChildFibers(wipFiber);
return;
}
instance.props = wipFiber.props;
instance.state = Object.assign({}, instance.state, wipFiber.partialState);
wipFiber.partialState = null;
const newChildElements = wipFiber.stateNode.render();
reconcileChildrenArray(wipFiber, newChildElements);
}
beginWork()
的作用有两个
- 创建 stateNode
- 拿到component children,并且调用
reconcileChildrenArray()
因为对不同类型component的处理方式不同, 这里分成了updateHostComponent
, updateClassComponent
两个函数。
updateHostComponennt
处理了host component 和 root component。如果fiber上没有DOM node则新建一个(仅仅是创建一个DOM节点,没有子节点,也没有插入到DOM中)。然后利用fiber props中的children去调用reconcileChildrenArray()
updateClassComponent
处理了用户创建的class component。如果没有实例则创建一个。并且更新了props和state,这样render就是可以计算出新的children。
updateClassComponent
并不是每次都调用render函数。这有点类似于shouldCompnentUpdate
函数。如果不需要调用render,就复制子节点。
现在我们有了newChildElements
, 我们已经准备好去创建child fiber。
reconcileChildrenArray()
注意,这里是核心。这里创建了work-in-progress 树和决定如何更新DOM
1 | / Effect tags |
首先我们确定newChildElements
是一个数组(并不像之前的diff算法,这次的算法的children总是数组,这意味着我们可以在render中返回数组)
然后,开始将old fiber中的children与新的elements做对比。还记得吗?fiber.alternate就是old fiber。new elements 来自于props.children
(function)和 render
(Class Component)。
reconciliation算法首先diff wipFiber.alternate.child 和 elements[0],然后是 wipFiber.alternate.child.sibling 和 elements[1]。这样一直遍历到遍历结束。
- 如果
oldFiber
和element
有相同的type。就通过old fiber创建新的。注意增加了UPDATE effectTag
- 如果这两者有不同的type或者没有对应的oldFiber(因为我们新添加了子节点),就创建新的fiber。注意新fiber不会有
alternate
属性和stateNode(stateNode就会在beginWork()
中创建)。还增加了PLACEMENT effectTag
。 - 如果这两者有不同的type或者没有对应的
element
(因为我们删除了一些子节点)。我们标记old fiberDELETION
。
cloneChildFibers()
updateClassComponent
中有一个特殊情况,就是不需要render,而是直接复制fiber。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27function cloneChildFibers(parentFiber) {
const oldFiber = parentFiber.alternate;
if (!oldFiber.child) {
return;
}
let oldChild = oldFiber.child;
let prevChild = null;
while (oldChild) {
const newChild = {
type: oldChild.type,
tag: oldChild.tag,
stateNode: oldChild.stateNode,
props: oldChild.props,
partialState: oldChild.partialState,
alternate: oldChild,
parent: parentFiber
};
if (prevChild) {
prevChild.sibling = newChild;
} else {
parentFiber.child = newChild;
}
prevChild = newChild;
oldChild = oldChild.sibling;
}
}
cloneChildFibers()
拷贝了old fiber的所有的子fiber。我们不需要增加effectTag
,因为我们确定不需要改变什么。
completeWork()
performUnitOfWork
, 当wipFiber 没有新的子节点,或者我们已经处理了所有的子节点时,我们调用completeWork
.
1 | function completeWork(fiber) { |
在 completeWork
中,我们新建了effects列表。其中包含了work-in-progress中所有包含effecTag
。方便后面处理。最后我们将pendingCommit指向了root fiber。并且在workLoop
中使用。
commitAllWork & commitWork
这是最后一件我们需要做的事情,改变DOM。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46function commitAllWork(fiber) {
fiber.effects.forEach(f => {
commitWork(f);
});
fiber.stateNode._rootContainerFiber = fiber;
nextUnitOfWork = null;
pendingCommit = null;
}
function commitWork(fiber) {
if (fiber.tag == HOST_ROOT) {
return;
}
let domParentFiber = fiber.parent;
while (domParentFiber.tag == CLASS_COMPONENT) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.stateNode;
if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) {
domParent.appendChild(fiber.stateNode);
} else if (fiber.effectTag == UPDATE) {
updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag == DELETION) {
commitDeletion(fiber, domParent);
}
}
function commitDeletion(fiber, domParent) {
let node = fiber;
while (true) {
if (node.tag == CLASS_COMPONENT) {
node = node.child;
continue;
}
domParent.removeChild(node.stateNode);
while (node != fiber && !node.sibling) {
node = node.parent;
}
if (node == fiber) {
return;
}
node = node.sibling;
}
}
commitAllWork
首先遍历了所有的根root effects。
- PLACEMENT。将dom插入到父节点上
- UPDATE。将新旧props交给
updateDomProperties()
处理。 - DELETION。如果是Host component。用removeChild()删除就好。如果是class Component,那就要删除fiber subTree下面的所有host Component。
一旦我们完成了所有的effects,就重置nextUnitOfWork
和pendingCommit
。work-in-progress tree就变成了old tree。并复制给_rootContainerFiber
。
这样我们完成了更新,并且做好了等待下一次更新的准备。