上篇文章我们讲解了beginWork的工作流程,而workLoop中另外个重要的函数就是completeWork,此篇文章会着重剖析completeWork阶段的工作内容。
workLoop是如何调度beginWork和completeWork,请翻阅第二篇文章不得不谈的workLoop
首先高度概括completeWork做的事:
- 针对不同fiber类型进行不同的处理
- 对比props,标记Update
- 收集subTreeFlags
老规矩还是先制定四个小目标:
- completeWork针对不同tag的处理方式
- HostComponent、HostText的节点创建与挂载
- Fiber中subTreeFlags的意义
- 位运算在React中的作用
一、不同fiber类型下completeWork的处理
我们暂且不去管具体函数中的操作,我们先进行归纳总结:
不同类型的tag都是需要去执行bubbleProperties收集flags,有的类型需要执行标记更新的操作, 而其他类型譬如HostComponent/HostText则需要创建实例dom对象并挂载。
接下来我们会针对不同类型进行举例:
二、HostComponent下的处理
简化后:
js
case HostComponent:
if (current !== null && wip.stateNode) {
// update 并且 dom节点可复用
// props是否发生变化 如果变化则标记update flags
if (diffProps(wip.pendingProps,current)){
markUpdate(wip);
}
} else {
// mount阶段 or update阶段下 节点需要新增
// 1.构建dom
const instance = createInstance(wip.type, newProps);
// 2.将dom插入到dom树中
appendAllChildren(instance, wip);
wip.stateNode = instance;
}
bubbleProperties(wip);
return null;
核心的逻辑
- 判断是否当前节点是复用且属性发生变化
- 新增节点需要创建dom实例并将子节点进行挂载
- 执行bubbleProperties收集subTreeFlags
接下来我们看看一些关键函数的实现
新增节点createInstance
简化后:
js
const createInstance = (type: string, props: Props): Instance => {
const element = document.createElement(type) as unknown;
updateFiberProps(element as DOMElement, props);
return element as DOMElement;
};
很简单其实就是调用了DomApi createElement去创建
挂载子节点到当前dom对象(appendAllChildren)
简化后:
js
const appendAllChildren = (parent: Container | Instance, wip: FiberNode) => {
// 通过遍历模拟递归 深度优先遍历模式
let node = wip.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node?.stateNode);
} else if (node.child !== null) {
// 往下找直到找到符合HostComponet or HostText的fiber节点
node.child.return = node;
node = node.child;
continue;
}
if (node === wip) return;
// 判断是否有兄弟节点 若有兄弟节点 则执行兄弟节点的判断逻辑(hostComponet or HostText 则插入,无则向下查找)
while (node.sibling === null) {
if (node.return === null || node.return === wip) {
return;
}
// 无兄弟节点且未归到顶部则往上回
node = node?.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
核心逻辑:
- 找到HostComponent or HostText节点,调用appendInitialChild插入
- 若node节点不符合,则向下找符合条件的节点
- 找到符合的节点后,再判断兄弟节点是否存在,若存在则让兄弟节点同样走一条和第二条
- 兄弟节点不存在,则向上回,直到顶点。
这样节点就完成了挂载~
bubbleProperties收集subTreeFlags
subTreeFlags是个二进制的数据,代表子节点具有哪些类型的标记
subTreeFlags的意义:
在commit阶段会针对不同的flags去进行不同的操作,譬如Placement(插入移动)、ChildDeletion(删除)、Update(更新)等等,由于收集了subTreeFlags,所以能很快的判断出来子树具有哪些标记,而不用遍历到每个节点fiber再判断flags.
简化后:
js
const bubbleProperties = (wip: FiberNode) => {
let subTreeFlags = Noflags;
let child = wip.child;
while (child !== null) {
subTreeFlags |= child.subTreeFlags;
subTreeFlags |= child.flags;
child.return = wip;
child = child.sibling;
}
wip.subTreeFlags |= subTreeFlags;
};
核心逻辑:
- 当子节点存在时候,收集子节点的flags
- 当子节点的兄弟节点存在,同样也需要收集其对应的flags
我们的收集其实就是subTreeFlags |= flags这样的操作,这个运算我们称之为位运算~
接下来简单介绍下位运算
位运算是在二进制的基础上对数字进行移动操作,位运算中有按位左移运算符<<、按位右移运算符>>、按位与[&]、按位或|等等
flags的数据结构就是二进制数据,可以参考上篇文章flags的介绍细说beginWork
具体怎么计算和移动大家可以自行去mdn查找,这里说下react为什么要使用位运算
- 位运算本身是很底层的运算,所以在react中相比于其他的运算它运行的更快,性能更好
- 位运算能够很好的表达状态集合,并且很方便的去处理集合的增删改查
这里举个栗子:
subTreeFlags初始状态下的值是NoFlags(0b0000000) ,当收集到Placement(0b0000001)、Update(0b0000010) 、ChildDeletion(0b0000100) ,通过|或运算可以新增状态得到0b0000111
当需要查询状态时候,只需要通过&运算 去判断是否存在某个状态,譬如查询Update,(subTreeFlags & Update) === NoFlags。
当需要去删除某个状态的时候,只需要通过**&= ~flags**去删除即可,譬如删除Placement,subTreeFlags&= ~Placement即可。
试想下,如果不用位运算,我们怎么去表达一个状态集合以及一个状态集合的增删改查呢?可能是数组结构表示集合,然后用数组的方法去进行增、删、改,但是从性能和操作性来说明显是位运算更优。
三、HostText下的处理
HostComponent一样,只不过创建dom对象方法不同 HostText源码位置
简化后:
js
case HostText:
if (current !== null && wip.stateNode) {
// update
const oldText = current.memorizedProps.content;
const newText = newProps.content;
if (oldText !== newText) {
markUpdate(wip);
}
} else {
// 构建dom
const instance = createTextInstance(newProps.content);
wip.stateNode = instance;
}
bubbleProperties(wip);
return null;
核心逻辑:
- 判断文本节点的content是否相同,不同则代表需要标记更新
- 构建文本节点
- 执行bubbleProperties收集subTreeFlags
第1和第3点上文已经阐述过,我们看看第二点~
createTextInstance其实非常简单就是调用了DomApi document.createTextNode
四、FunctionComponent、Fragment等其他类型
只需要执行bubbleProperties收集subTreeFlags FC/FR源码位置
至此我们完成了四个小目标~
总结:
- completeWork核心的目标就是向父级fiber节点收集flags,针对某些类型的fiber进行节点创建和挂载。
- HostComponent、HostText的节点创建与挂载本质上是DomApi的能力。
- 父级fiber中的subTreeFlags能够很好的判断出来子树当中有哪些标记。
- React中的位运算能够更好的表示状态的集合、状态的增删改查和更快的速度。