渐进式剖析React源码(5):详解completeWork

上篇文章我们讲解了beginWork的工作流程,而workLoop中另外个重要的函数就是completeWork,此篇文章会着重剖析completeWork阶段的工作内容。

workLoop是如何调度beginWork和completeWork,请翻阅第二篇文章不得不谈的workLoop

首先高度概括completeWork做的事:

  1. 针对不同fiber类型进行不同的处理
  2. 对比props,标记Update
  3. 收集subTreeFlags

老规矩还是先制定四个小目标:

  1. completeWork针对不同tag的处理方式
  2. HostComponent、HostText的节点创建与挂载
  3. Fiber中subTreeFlags的意义
  4. 位运算在React中的作用

一、不同fiber类型下completeWork的处理

completeWork源码位置

我们暂且不去管具体函数中的操作,我们先进行归纳总结:

不同类型的tag都是需要去执行bubbleProperties收集flags,有的类型需要执行标记更新的操作, 而其他类型譬如HostComponent/HostText则需要创建实例dom对象并挂载。

接下来我们会针对不同类型进行举例:

二、HostComponent下的处理

completeWork处理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;

核心的逻辑

  1. 判断是否当前节点是复用且属性发生变化
  2. 新增节点需要创建dom实例并将子节点进行挂载
  3. 执行bubbleProperties收集subTreeFlags

接下来我们看看一些关键函数的实现

新增节点createInstance

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)

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;
	}
}

核心逻辑:

  1. 找到HostComponent or HostText节点,调用appendInitialChild插入
  2. 若node节点不符合,则向下找符合条件的节点
  3. 找到符合的节点后,再判断兄弟节点是否存在,若存在则让兄弟节点同样走一条和第二条
  4. 兄弟节点不存在,则向上回,直到顶点。

这样节点就完成了挂载~

bubbleProperties收集subTreeFlags

subTreeFlags是个二进制的数据,代表子节点具有哪些类型的标记

subTreeFlags的意义

在commit阶段会针对不同的flags去进行不同的操作,譬如Placement(插入移动)、ChildDeletion(删除)、Update(更新)等等,由于收集了subTreeFlags,所以能很快的判断出来子树具有哪些标记,而不用遍历到每个节点fiber再判断flags.

bubbleProperties源码位置

简化后:

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;
};

核心逻辑:

  1. 当子节点存在时候,收集子节点的flags
  2. 当子节点的兄弟节点存在,同样也需要收集其对应的flags

我们的收集其实就是subTreeFlags |= flags这样的操作,这个运算我们称之为位运算~

接下来简单介绍下位运算

位运算是在二进制的基础上对数字进行移动操作,位运算中有按位左移运算符<<、按位右移运算符>>、按位与[&]、按位或|等等

flags的数据结构就是二进制数据,可以参考上篇文章flags的介绍细说beginWork

具体怎么计算和移动大家可以自行去mdn查找,这里说下react为什么要使用位运算

  1. 位运算本身是很底层的运算,所以在react中相比于其他的运算它运行的更快,性能更好
  2. 位运算能够很好的表达状态集合,并且很方便的去处理集合的增删改查

这里举个栗子:

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;

核心逻辑:

  1. 判断文本节点的content是否相同,不同则代表需要标记更新
  2. 构建文本节点
  3. 执行bubbleProperties收集subTreeFlags

第1和第3点上文已经阐述过,我们看看第二点~

createTextInstance其实非常简单就是调用了DomApi document.createTextNode

四、FunctionComponent、Fragment等其他类型

只需要执行bubbleProperties收集subTreeFlags FC/FR源码位置

至此我们完成了四个小目标~

总结:

  1. completeWork核心的目标就是向父级fiber节点收集flags,针对某些类型的fiber进行节点创建和挂载。
  2. HostComponent、HostText的节点创建与挂载本质上是DomApi的能力。
  3. 父级fiber中的subTreeFlags能够很好的判断出来子树当中有哪些标记。
  4. React中的位运算能够更好的表示状态的集合、状态的增删改查和更快的速度。
相关推荐
qq_3643717219 分钟前
Vue 内置组件 keep-alive 中 LRU 缓存淘汰策略和实现
前端·vue.js·缓存
y先森1 小时前
CSS3中的弹性布局之侧轴的对齐方式
前端·css·css3
y先森6 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy6 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189116 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿7 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡8 小时前
commitlint校验git提交信息
前端
虾球xz9 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇9 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒9 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript