翻译自这里:https://engineering.hexacta.com/didact-fiber-incremental-reconciliation-b2fe028dcaec

React16已经发布了,其内部重写了很多代码,内部结构也发生了一些变化,有一些新的特性也随之推出。我们在之前系列里写的代码多多少少有些过时了。在这一节,我们将会根据React16的最新结构来重写大部分的代码,代码结构和变量命名我们也会尽量按照React16的来。我们的代码暴露了一下几个API:

  • Didact.createElement
  • Didact.render()(用来DOM渲染)
  • Didact.Component(带有setState(),但没有context及其他生命周期方法)

以上API不需要的东西我们不会涉及。

如果你想直接看代码及运行效果,可以看这里,或者访问代码库

好了,先来解释下以前的代码为什么需要重写。

Why Fiber

我们不会解释的很详细,如果你想具体了解,可以看一下这个list

当浏览器的主线程一直被某一任务占用时,其他任务就无法去执行,直到占用主线程的任务完成,其他任务才有可能被执行。

我做了一个demo来展示上面提到的问题。为了保持星球的持续转动,主线程至少每隔16ms就需要空闲出来以渲染页面。假如主线程一直被其他任务占用,比如占用了200ms,你就会发现动画有一些丢帧(通俗的说就是卡顿),在主线程被占用时星球会停着不动。

那么是什么一直占用着主线程,使得主线程无法去执行渲染任务以至于页面有卡顿呢?

还记得我们写的一致性校验的代码么?一旦开始一致性校验,过程就不会停止。此时,如果其他任务需要执行,那么只有排队等待。而且,因为一致性校验是一个递归的规程,所以很难暂停。接下来我们将会使用一种新的数据结构来重写一致性校验的代码,并用循环来代替递归。

Scheduling micro-tasks

我们需要把__任务__分割成更细小的__任务片__,以单位任务片的方式来执行这些任务,在执行的间隙,主线程可以去执行有更高优先级的其他任务,结束之后再回来继续执行剩余的任务片。

我们将定义一个requestIdleCallback()方法来完成上述功能。这个方法会维护一个回调,这个回调是下次浏览器空闲时需要去执行的任务。在执行这个回调时还会传入一个deadline参数,用来描述当前有多少时间可以用来执行这个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const ENOUGH_TIME = 1; // 毫秒

let workQueue = [];
let nextUnitOfWork = null;

function schedule(task){
workQueue.push(task);
requestIdleCallback(performWork);
}

function performWork(deadline){
if(!nextUnitOfWork){
nextUnitOfWork = workQueue.shift();
}

while(nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME ){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

if(nextUnitOfWork || workQueue.length > 0){
requestIdleCallback(performWork);
}
}

真正执行任务的是performUnitOfWork这个方法,我们的一致性校验算法也需要写到这个方法里面。这个方法会执行任务片并返回下次需要执行的任务片。

我们使用fiber来跟踪任务片的执行。

The fiber data structure

每个组件(__注意,这里的组件不仅是用户定义的组件,也代表浏览器原生的HTML标签__)我们都会为其创建一个fiber。nextUnitOfWork指向的是下一次我们要运行的fiber。performUnitOfWork会执行当前的fiber并在执行结束后返回一个新的fiber。跟紧我,接下来我将详细解释一下。

先来看下fiber的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
let 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: []
}

看起来只是一个很普通的JS对象。

parentchild以及sibling将被用来构建一颗描述组件的fiber树。stateNode指向一个DOM元素或者是用户定义的组件的实例。

变量说明

上面的图片展示了我们需要支持的三种类型的组件:

  • bpi这一类的fiber我们称为__host components__,用tag:HOST_COMPONENT来表示。这一类fiber的type属性值为一个字符串(即对应的html元素标签名)。props则放置着对应元素的属性和事件。
  • Foo对应的fiber我们称为__class components__,对应的tag标签值为CLASS_COMPONENT。这一类fiber的type属性值为指向用户定义的组件类的引用。
  • div对应的fiber我们称为__host root__。host root和host component都含有一个DOM元素作为stateNode的属性值,但host root作为fiber树的根,将会受到一些特别的对待。我们使用tag:HOST_ROOT来区分host root。注意到,此类fiber的stateNode对应的DOM节点将会被传入到Didact.render()中。

另一个比较重要的属性是alternate。__大多数情况下我们代码中存在两棵fiber树:一颗对应着已经渲染到页面的DOM,我们称之为current tree或者old tree;另一颗为我们更新(调用setState()或者Didact.render())过程中构建的树,我们称之为work-in-progress tree。__(这两棵树的节点都是一个个fiber)

work-in-progress tree不会和old tree共享fiber。一旦work-in-progress tree构建结束,对应的DOM都被渲染完毕,work-in-progress tree就会变成old tree。

alternate用来连接work-in-progress tree上的fiber对应的old tree上的fiber。一个fiber与它的alternate指向的old tree上的fiber拥有相同的tagtypestateNode。当我们在渲染一个新的结构时,对应的fiber不会含有alternate属性。

接下来是effects数组和effectTag。当work-in-progress tree上的某一个fiber需要对DOM做一些变更时,我们会给这个fiber设置effecttTag属性,取值有三种:PLACEMENTUPDATE,或者DELETION。为了更方便的实施DOM的变更,我们将当前fiber下含有effectTag的子fiber都保存在effects数组中。

上面说了比较多的概念,一时理解有些困难,如果跟不上也不要担心,下面我们在实际代码中来了解一下fiber。

Didact call hierarchy

我们通过流程图来感知一下即将要写的代码的调用层次:

fiber流程

我们会从render()或者setState()开始,到commitAllWork()结束。

Old code

在开始重写之前先来回顾下以前写的代码。

Element creation and JSX中我们编写了crerateElement()方法用来转译JSX。这个方法不需要改动,元素的结构也没有发生变化。如果你不知道我们所说的元素,typepropschildren是什么,那么你需要回顾一下之前的内容。

Instances, reconciliation and virtual DOM这一节我们写了updateDomProperties()方法用来更新DOM节点的属性。我们还写了createDomElement()方法用来创建DOM节点。这两个方法你都可以在dom-utils.js中看到。

Components and state这一节我们编写了Component基础类。这个类需要一些改动。setState()方法中需要去调用scheduleUpdate()方法。此外还要创建一个createInstance方法来代替之前的createPublicInstance()方法,createInstance创建的实例保存了自身对应的fiber的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class 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分类标签
const HOST_COMPONENT = 'host';
const CLASS_COMPONENT = 'class';
const HOST_ROOT = 'root';

// 全局变量
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); // 延迟调用performWork
}

updateQueue数组用来盛装要实施的更新,每次调用render()或者scheduleUpdate()方法都会往updateQueue中增加一个更新操作。每个更新操作携带的信息都不相同,我们将会在接下来的resetNextUnitOfWork()方法中看到如何去实施这些更新。

在把更新放到队列中之后,我们对performWork()做了一个延迟调用(意思是在浏览器空闲的时候调用)。

performWork()&workLoop()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const ENOUGH_TIME = 1;

function performWork(deadline){
workLoop(deadline);
if(nextUnitOfWork || updateQueue.length > ){
requestIdleCallback(performWork);
}
}

function workLoop(deadline){
if(!nextUnitOfWork){
resetNextUnitOfWork(); // 产生第一个nextUnitOfWork
}
while(nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if(pendingCommit){
commitAllWork(pendingCommit);
}
}

requestIdleCallback()方法会将一个deadline传入目标方法(就是performWork())中,并执行这个方法。performWork()会将接收到的deadline传递给workLoop()方法,workLoop()执行结束后,performWork()中剩下的代码还会检查是否还有等待完成的任务,如果有,则会在浏览器空闲的时候再次调用自己。

workLoop()会监视着deadline参数,如果deadline太短,方法内部会自动停止循环,并保持nextUnitOfWork不做改变,下次会继续执行这个任务。

ENOUGH_TIME是一个代表1ms的常量,通过deadline.timeRemaining()与ENOUGH_TIME的比较来判断是否有足够的时间来执行当前这个任务。如果performUnitOfWork()所需要的时间超过ENOUGH_TIME,我们会适当增加deadline的值。deadline只是浏览器所建议的一个时间,所以增加几毫秒时没有什么问题的。

(We use ENOUGH_TIME (a 1ms constant, same as React’s) to check if deadline.timeRemaining() is enough to run another unit of work or not. If performUnitOfWork() takes more than that, we will overrun the deadline. The deadline is just a suggestion from the browser, so overrunning it for a few milliseconds is not that bad.—-这一段说实话我不太明白什么意思)

performUnitOfWork()会为当前的更新操作构建一颗work-in-progress tree,并会比较出需要对DOM实施的变更。这些操作都是逐步进行的,每次构建一个fiber节点。

performUnitOfWork()结束了当前更新所需要做的任务之后,会返回null(这样循环就结束了)并将要实施的更新操作保存在pendingCommit变量中。最后,commitAllWork()会从pendingCommit中取出effects,并对对应的DOM实施变更操作。

注意到commitAllWork()是在循环外面调用的。performUnitOfWork()的任务完成后并没有对DOM进行变更(只是记录),所以它是可以分开执行的,而commitAllWork()是会对DOM进行改变的,所以为了保证代码和UI显示一致,需要一次性将commitAllWork()执行完毕。

说了这么多,我们依然不知道第一个nextUnitOfWork来自于哪里。

resetUnitOfWork()

resetUnitOfWork()方法会接收一个更新操作并将其转化为nextUnitOfWork(其实就是根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
27
28
29
30
31
32
33
function resetNextUnitOfWork(){
const update = updateQueue.shift();
if(!update){
return;
}
// 将更新操作中携带的state复制给对应fiber
if(update.partialState){ // 通过setState()更新才会有partialState
update.instance.__fiber.partialState = update.partialState;
}

const root = update.from == HOST_ROOT
? update.dom.__rootContainerFiber
: getRoot(update.instance.__fiber);

// 注意看,这时候的fiber都是没有child属性的,返回的是根节点的fiber。
// 就是old tree的根节点
nextUnitOfWork = {
tag: HOST_ROOT,
// 如果是render()引起的话,stateNode从update.dom取值,否则从root.stateNode取值
stateNode: update.dom || root.stateNode,
// props同理
props: update.newProps || root.props;
alternate: root
};
}

function getRoot(fiber){
let node = fiber;
while(node.parent){
node = node.parent;
}
return node;
}

首先,resetNextUnitOfWork()会从updateQueue头部取出一个更新操作,如果这个更新操作携带有partialState信息,那么将该信息复制到此次更新对应组件实例的fiber上,在稍后调用组件的render()方法时会用到这个partialState

接下来是寻找old fiber tree的根节点。如果此次更新是整个应用第一次调用render()(第一次渲染)引起的,则不存在根fiber节点,所以root = null;如果此次更新是由非第一次调用render()方法引起的,我们则可以通过DOM节点的__rootContainerFiber属性找到根fiber节点;如果此次更新是由setState()引起的,则需要从当前fiber往上查找,直到找到没有parent属性那个fiber节点,即为根fiber节点。

找完根fiber节点后,我们给nextUnitOfWork赋值一个新的fiber。__这个fiber是一棵新work-in-progress tree的根fiber节点__(因为是本次渲染的第一个nextUnitOfWork,所以是根fiber节点)。

如果不存在old root(说明这是初次渲染),则stateNode就是传入render()方法的那个DOM节点,props是来自于此次渲染的newPropsnewPropschildren数组含有的其他元素也会被传入到render()方法中。alternate属性将会是null(因为是初次渲染,不存在old tree)。

如果存在old root(真正意义上的更新操作,增量渲染),则stateNode就是上一次渲染的根DOM节点,props同样会从newProps取值,如果newPropsnull的话,则从old root上取值。alternate指向的就是old root。

现在已经有了work-in-progress tree个根fiber节点,接下来我们从这个根节点开始构建work-in-progress fiber tree。

performUnitOfWork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function performUnitOfWork(wipFiber){
beginWork(wipFiber);
if(wipFiber.child){
return wipFiber.child;
}

// 如果没有子元素,则寻找兄弟元素
let uow = wipFiber;
while(uow){
completeWork(uow); // 如果wipFiber没有子元素,或者当所有子节点都被处理完毕时会被调用
if(uow.sibling){
return uow.sibling; // 返回找到的兄弟元素,构建一个节点。
}
uow = uow.parent;
}
}

performUnitOfWork()方法会贯穿于整棵fiber树的构建过程。

beginWork()用来为传入的fiber创建子fiber节点,然后将第一个子fiber节点返回作为下一个nextUnitOfWork参数。

如果当前已有fiber不存在子节点,则执行completeWork()方法,然后返回其兄弟节点作为下一个nextUnitOfWork参数。

如果当前fiber连兄弟节点也不存在,则向上查找,并逐层调用completeWork方法,直到找到并返回兄弟节点或者到达根节点。

fiber树的创建过程中,performUnitOfWork()会被调用多次。

我们会以深度优先的原则去创建一棵fiber树。从根节点开始,遍历每个节点的第一个子fiber(即child属性所指向的对象)。当到达某一个fiber节点时,我们会将该节点作为入参去调用performUnitOfWork();如果某一fiber节点不含有子节点,则往右移动找寻兄弟节点,如果不存在兄弟节点则往上寻找祖先元素的兄弟节点,如此进行直到找到兄弟节点并将其带入到performUnitOfWork()中执行或者到达根节点。然后以当前节点为起点,继续按照深度优先的原则去遍历和创建fiber节点,整个过程会调用performUnitOfWork()多次,直到整棵树创建完毕。(可以在这里fiber-debugger查看更生动的描述)

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
function 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){
// 调用构造函数,创建实例
instance = wipFiber.stateNode = createInstance(wipFiber)
} else if(wipFiber.props == instance.props && !wipFiber.partialState){
cloneChildFiber(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()做了两件事:

  • 如果传入的fiber没有stateNode节点的话为其创建一个。
  • 获取组件的子元素,并将它们传入reconcileChildrenArray()中。

因为要考虑不同的组件类型,所以干脆就分成了两个方法来处理:updateHostComponent()updateClassComponent()

updateHostComponent()方法用来处理host components及root component两种类型组件。如果传入的fiber不含有stateNode,则方法会为其创建一个(只有一个节点且不含有子元素,更不会添加到页面上),然后方法会从fiber的props属性中取出children并传入reconcileChildrenArray()中去执行性。

传入updateClassComponent()方法的fiber如果没有实例的话,则方法内部首先会通过调用组件的构建函数为其创建一个实例。创建出来的实例会被添加最新的 propsstate属性,之后调用实例的render()方法来获取最新的子元素。如果传入进来的fiber有对应的实例,则说明不是一个新的节点。这时候如果fiber的props和实例的props相等,并且fiber上不带有partialState属性,则说明节点前后没有发生变化(相当于一个简易版的shouldComponentUpdate()),不需要重新渲染,直接克隆该节点到work-in-progress tree就可以了。

现在我们有了子元素newChildElements,可以继续向下构建work-in-progress fiber tree了。

reconcileChildrenArray()

reconcileChildrenArray()是一个比较核心的代码,fiber树的构建以及要对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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const PLACEMENT = 1;
const DELETION = 2;
const UPDATE = 3;

function arrify(val){
return val == null ? null : Array.isArray(val) ? val : [val];
}

function reconcileChildArray(wipFiber, newChildElements){
const elements = arrify(newChildElements);

let index = 0;
let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
let newFiber = null;
while(index < elements.length || oldFiber != null){
const prevFiber = newFiber;
const element = index < elements.length && elements[index];
const sameType = oldFiber && element && element.type == oldFiber.type;

if(sameType){ // 如果前后fiber类型一样,说明是一个更新操作
newFiber = {
type: oldFiber.type,
tag: oldFiber.tag,
stateNode: oldFiber.stateNode,
props: element.props,
parent: wipFiber,
alternate: oldFiber,
partialState: oldFiber.partialState,
effectTag: UPDATE
};
}

if(element && !sameType){
newFiber = {
type: element.type,
tag:
typeof element.type === 'string' ?HOST_COMPPONENT: CLASS_COMPONENT,
props: element.props,
parent: wipFiber,
effectTag: PLACEMENT
};
}

if(oldFiber && !sameType){
oldFiber.effectTag = DELETION;
wipFiber.effects = wipFiber.effects || [];
wipFiber.effects.push(oldFiber); // 记录oldFiber的删除操作
}

if(oldFiber){
oldFiber = oldFiber.sibling;
}

if(index == 0){
wipFiber.child = newFiber; // fiber的child属性指向的是第一个子fiber
} else if(prevFiber && element){
prevFiber.sibling = newFiber; // 剩下的子fiber相互之间通过sibling属性关联
}

index++;
}
}

一开始我们要确保传入的newChildElements是个数组。(这和我们之前写的一致性校验的算法有些不同,之前的render()方法返回的是一个对象,不支持数组,现在这个是子元素全部放在数组中,也就是说我们可以在组件的render()方法中直接返回一个数组了)。

接下来将wipFiber.alernate(即old fiber tree)的子节点与wipFiber.props.children对应的元素或者执行wipFiber.stateNode.render()后返回的元素进行比较。(fiber节点与元素进行比较,这地方的元素就是用对象表示的JSX)

在比较时,是oldFiber-element成对的比较。先是第一个fiber子节点(oldFiber = wipFiber.alternate.child)与第一个子元素(elements[0])比较,比较结束后,oldFiber赋值为其自身的兄弟节点(oldFiber = oldFiber.sibling),然后继续与第二个子元素比较(elements[1])。比较结束后再次执行oldFiber = oldFiber.sibling,然后再将oldFiberelements[2]比较。以此方式比较下去:

  • 如果oldFiber和对应的element有相同的type,则代表oldFiber.stateNode可以被重用,我们会基于oldFiber来创建一个新的fiber,并将新fiber的effectTag属性设置为UPDATE,然后将这个新的fiber添加到work-in-progress tree上。
  • 如果element与对应的oldFibertype属性值不一样,或者说当前的element没有对应的oldFiber(新增元素的情况),我们会根据element上含有的信息新建一个fiber。注意,这个新建的fiber没有alternate属性,也没有stateNode属性(stateNode属性会在beginWork()中被创建)。我们会将这个新建fiber的effectTag设置为PLACEMENT
  • 如果element与对应的oldFibertype属性值不一样,或者oldFiber没有对应的element(说明有元素需要被删除),我们会将oldFibereffectTag设置为DELETION。由于oldFiber并不是work-in-progress tree的一部,我们需要将其添加到wipFiber.effects列表中以防止丢失。

我们并没有像React那样使用key属性来做一致性校验,如果子元素只是换了一个位置的话,我们的代码并不会做特殊处理。

cloneChildFibers()

updateClassComponent()方法中考虑了一种特殊的情况,当某个节点不需要更新时,可以直接将对应old filber tree上的节点拷贝到work-in-progress tree上,省去了一致性校验的过程。

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
function cloneChildFibers(parentFiber){
const oldFiber = parentFiber.alternate; // 获取传入的fiber对应的oldFiber
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; // 通过sibling(兄弟)关系循环所有子元素
}
}

cloneChildFibers()会克隆传入的wipFiber.alternate指向的fiber的所有子元素,并将它们添加到work-in-progress tree上。因为确定这些节点没有发生什么变化,所以并不需要添加effectTag属性。

completeWork()

performUnitOfWork()中,当wipFiber没有子元素或者当所有子元素都被执行完毕后,我们就会调用completeWork()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function completeWork(fiber){
if(fiber.tag == CLASS_COMPONENT){ // 如果是一个类组件
fiber.stateNode.__fiber = fiber;
}
// 每次更新都要重新构建一整颗fiber树
if(fiber.parent){
const childEffects = fiber.effects || [];
const thisEffect = fiber.effectTag != null ? [fiber] : [];
const parentEffects = fiber.parent.effects || [];
fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
} else {
pendingCommit = fiber;
}
}

completeWork()首先更新传入的fiber对应实例类组件对象的fiber引用(可能不一定非要在这个地方,但代码里需要这么几行代码)。

接下来,方法内部会构建一个effcts列表,这个列表会将work-in-progres sub-tree上含有effectTag属性的fiber都包含在内(old sub-tree上含有DELETION effectTag的fiber也会被包含在这个effects列表中)。通过这样的effects列表,根fiber的efffects会包含所有带有effectTag的fiber。

最后,当fiber不含有parent属性时,说明我们已经到达了work-in-progress tree的根部,此时,我们已经完成了此次的更新操作所需要的工作,并收集了所有的effects。接下来,我们将根节点赋值给pendingCommit,然后workLoop()会去调用commmitAllWork()来完成更新。

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
46
47
48
49
50
51
52
function commitAllWork(fiber){
fiber.effects.forEach(f => {
commitWork(f);
})
// 根fiber节点对应的DOM节点有个__rootContainerFiber属性引用着根fiber
fiber.stateNode.__rootContainerFiber = fiber;
nextUnitOfWork = null;
pendingCommit = null;
}

function commitWork(fiber){
if(fiber.tag == HOST_ROOT){
return;
}

let domParentFiber = fiber.parent;
// 寻找一个dom类型的祖先fiber(stateNode属性对应为原生DOM)
while(domParentFiber.tag == CLASS_COMPONENT){
domParentFiber = domParentFiber.parent;
}

// 有了DOM才好去调用DOM的那些方法去操作DOM
const domParent = domParentFiber.stateNode;

if(fiber.effectTag == PLACEMENT && fiber.tag = HOST_COMPONENT){ // 添加一个DOM
domParent.appendChild(fiber.stateNode);
} else if(fiber.effectTag == UPDATE){ // 更新一个DOM
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);
// 如果node不等于fiber,并且没有兄弟节点,说明已经删除完毕
while(node != fiber && !node.sibling){
node = node.parent; // 删除完毕后node重置为刚开始的值
}
if(node == fiber){
return;
}
node = node.sibling;
}

}

commitAllWork()会循环根fiber上的effects数组,针对每个元素都会去调用commitWork()方法。commitWork会检查每个fiber的effectTag属性:

  • effectTag:PLACEMENT:先找到父DOM节点,然后直接将fiber的stateNode属性对应的DOM节点直接append到父DOM节点下。
  • effectTag:UPDATE:将stateNode及其对应的新旧props传入updateDomProperties中,方法内部再去对节点进行更新。
  • effectTag:DELETION:如果当前的fiber是一个host component(stateNode属性为一个原生DOM节点),这种时候直接通过其父节点调用removeChild()方法来删除该节点;如果当前的fiber是一个class component,在进行删除之前,需要找到组件对应的sub fiber-tree上所有的host component,然后再进行删除。

当前的effects都被实施后,需要重置nextUnitOfWorkpendingCommit,work-in-progress tree也变成了old tree,所以我们会将它的根节点设置到其对应DOM节点的__rootContainerFiber属性上。这些都做完后,当前的更新就都完成了,我们可以进行下一个了。

Running Didact

把上面我们写的代码加入到Didact中,然后暴露公共API即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function importDidact(){
//...
// All the code we wrote
// ...

return {
createElement,
render,
Component
};
}

/**@jsx Didact.createElement*/

class HelloMessage extends Didact.Component{
render(){
return <div>Hello {this.props.name}</div>;
}
}

Didact.render(
<HelloMessage name='John' >,
document.getElementById('container')
)

What’s next?

React的很多特性Didact都没有,其中我比较感兴趣的就是根据优先级来调度更新:

1
2
3
4
5
6
7
8
module.exports = {
NoWork: 0,
SynchronousPriority: 1, // For controlled text inputs.Synchronous side-effects.
TaskPriority: 2,// Completes at the end of the current tick
HighPriority: 3,//Interaction that needs to complete preety soon to feel responsive
LowPriority: 4, //Data fetching, or result from updating stores
OffscreenPriority: 5 //Won't be visible but do the work in case it becomes visible
}

所以如果有下一节的话可能就会涉及到上面的内容。

以上就是全部内容了。如果你喜欢,别忘了点赞,或者推特上关注我

记得留下你的评论。

感谢阅读,喜欢的记得点击star。