渐进式剖析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中的位运算能够更好的表示状态的集合、状态的增删改查和更快的速度。
相关推荐
挣扎与觉醒中的技术人12 分钟前
【技术干货】三大常见网络攻击类型详解:DDoS/XSS/中间人攻击,原理、危害及防御方案
前端·网络·ddos·xss
zeijiershuai17 分钟前
Vue框架
前端·javascript·vue.js
写完这行代码打球去19 分钟前
没有与此调用匹配的重载
前端·javascript·vue.js
华科云商xiao徐19 分钟前
使用CPR库编写的爬虫程序
前端
狂炫一碗大米饭22 分钟前
Event Loop事件循环机制,那是什么事件?又是怎么循环呢?
前端·javascript·面试
IT、木易23 分钟前
大白话Vue Router 中路由守卫(全局守卫、路由独享守卫、组件内守卫)的种类及应用场景
前端·javascript·vue.js
顾林海24 分钟前
JavaScript 变量与常量全面解析
前端·javascript
程序员小续24 分钟前
React 组件库:跨版本兼容的解决方案!
前端·react.js·面试
乐坏小陈25 分钟前
2025 年你希望用到的现代 JavaScript 模式 【转载】
前端·javascript
生在地上要上天25 分钟前
从600行"状态地狱"到可维护策略模式:一次列表操作限制重构实践
前端