引言
本系列是讲述从0开始实现一个react18的基本版本。由于React
源码通过Mono-repo 管理仓库,我们也是用pnpm
提供的workspaces
来管理我们的代码仓库,打包我们使用rollup
进行打包。
仓库地址
具体章节代码3个commit
本章我们主要讲解通过useState
状态改变,引起的单节点update
更新阶段的流程。
对比Mount阶段
对比我们之前讲解的mount
阶段,update
阶段也会经历大致的流程, 只是处理逻辑会有不同:
之前的章节我们主要讲了reconciler
(调和) 阶段中mount
阶段:
beginWork
:向下调和创建fiberNode
树,completeWork
:构建离屏DOM树以及打subtreeFlags
标记。commitWork
:根据placement
创建domuseState
: 对应调用mountState
这一节的update
阶段如下:
begionWork
阶段:
- 处理
ChildDeletion
的删除的情况 - 处理节点移动的情况 (abc -> bca)
completeWork
阶段:
- 基于
HostText
的内容更新标记更新flags
- 基于
HostComponent
属性变化标记更新flags
commitWork
阶段:
- 基于
ChildDeletion
, 遍历被删除的子树 - 基于
Update
, 更新文本内容
useState
阶段:
- 实现相对于
mountState
的updateState
下面我们分别一一地实现单节点的update
更新流程
beginWork流程
对于单一节点的向下调和流程,主要在childFibers
文件中,分2种,一种是文本节点的处理reconcileSingleTextNode
, 一种是标签节点的处理reconcileSingleElement
。
复用fiberNode
在update
阶段的话,主要有一点是要思考如何复用之前mount
阶段已经创建的fiberNode
。
我们先以reconcileSingleElement
为例子讲解。
当新的ReactElement
的type 和 key都和之前的对应的fiberNode
都一样的时候,才能够进行复用。我们先看看reconcileSingleElement
是复用的逻辑。
function reconcileSingleElement(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
element: ReactElementType
) {
const key = element.key;
// update的情况 <单节点的处理 div -> p>
if (currentFiber !== null) {
// key相同
if (currentFiber.key === key) {
// 是react元素
if (element.$$typeof === REACT_ELEMENT_TYPE) {
// type相同
if (currentFiber.type === element.type) {
const existing = useFiber(currentFiber, element.props);
existing.return = returnFiber;
return existing;
}
}
}
}
}
- 首先我们需要判断
currentFiber
是否存在,当存在的时候,说明是进入了update
阶段。 - 根据
currentFiber
和element
的tag 和 type判断,如果相同才可以复用。 - 通过双缓存树(
useFiber
)去复用fiberNode。
useFiber
复用的逻辑本质就是调用了useFiber
, 本质上,它是通过双缓存书指针alternate
,它接受已经渲染对应的fiberNode
以及新的Props
巧妙的运用我们之前创建wip
的逻辑,可以很好的复用fiberNode
。
function useFiber(fiber: FiberNode, pendingProps: Props): FiberNode {
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0;
clone.sibling = null;
return clone;
}
对于reconcileSingleTextNode
删除旧的和新建fiberNode
当不能够复用fiberNode
的时候,我们除了要像mount
的时候新建fiberNode
(已经有的逻辑),还需要删除旧的fiberNode
。
我们先以reconcileSingleElement
为例子讲解。
在beginWork
阶段,我们只需要标记删除flags
。以下2种情况我们需要额外的标记旧fiberNode
删除
key
不同key
相同,type
不同
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode) {
if (!shouldTrackEffects) {
return;
}
const deletions = returnFiber.deletions;
if (deletions === null) {
// 当前父fiber还没有需要删除的子fiber
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
我们将需要删除的节点,通过数组形式赋值到父节点deletions
中,并标记ChildDeletion
有节点需要删除。
对于reconcileSingleTextNode
, 当渲染视图中是HostText
就可以直接复用。整体代码如下:
function reconcileSingleTextNode(
returnFiber: FiberNode,
currentFiber: FiberNode | null,
content: string | number
): FiberNode {
// update
if (currentFiber !== null) {
// 类型没有变,可以复用
if (currentFiber.tag === HostText) {
const existing = useFiber(currentFiber, { content });
existing.return = returnFiber;
return existing;
}
// 删掉之前的 (之前的div, 现在是hostText)
deleteChild(returnFiber, currentFiber);
}
const fiber = new FiberNode(HostText, { content }, null);
fiber.return = returnFiber;
return fiber;
}
completeWork流程
当在beginWork
做好相应的删除和移动标记后,在completeWork
主要是做更新的标记。
对于单一的节点来说,更新标记分为2种,
- 第一种是文本元素的更新,主要是新旧文本内容的不一样。
- 第二种是类似div的属性等更新。这个我们下一节进行讲解。
这里我们只对HostText
中的类型进行讲解。
case HostText:
if (current !== null && wip.stateNode) {
//update
const oldText = current.memoizedProps.content;
const newText = newProps.content;
if (oldText !== newText) {
// 标记更新
markUpdate(wip);
}
} else {
// 1. 构建DOM
const instance = createTextInstance(newProps.content);
// 2. 将DOM插入到DOM树中
wip.stateNode = instance;
}
bubbleProperties(wip);
return null;
从上面我们可以看出,我们根据文本内容的不同,进行当前节点wip
进行标记。
function markUpdate(fiber: FiberNode) {
fiber.flags |= Update;
}
commitWork流程
通过beginWork
和completeWork
之后,我们得到了相应的标记。在commitWork
阶段,我们就需要根据相应标记去处理不同的逻辑。本节主要讲解更新
和删除
阶段的处理。
更新update
在之前的章节中,我们讲解了commitWork
的mount
阶段,我们现在根据update
的flag进行逻辑处理。
// flags update
if ((flags & Update) !== NoFlags) {
commitUpdate(finishedWork);
finishedWork.flags &= ~Update;
}
commitUpdate
对于文本节点,commitUpdate
主要是根据新的文本内容,更新之前的dom的文本内容。
export function commitUpdate(fiber: FiberNode) {
switch (fiber.tag) {
case HostText:
const text = fiber.memoizedProps.content;
return commitTextUpdate(fiber.stateNode, text);
}
}
export function commitTextUpdate(textInstance: TestInstance, content: string) {
textInstance.textContent = content;
}
删除ChildDeletion
在beginWork
过程中,对于存在要删除的子节点,我们会保存在当前父节点的deletions
, 所以在删除阶段,我们需要根据当前节点的deletions
属性进行对要删除的节点进行不同的处理。
// flags childDeletion
if ((flags & ChildDeletion) !== NoFlags) {
const deletions = finishedWork.deletions;
if (deletions !== null) {
deletions.forEach((childToDelete) => {
commitDeletion(childToDelete);
});
}
finishedWork.flags &= ~ChildDeletion;
}
如果当前节点存在要删除的子节点的话,我们需要对每一个子节点进行commitDeletion
的操作。
commitDeletion
commitDeletion
函数的是对每一个要删除的子节点进行处理。它的主要功能有几点:
- 对于不同类型的
fiberNode
, 当节点删除的时候,自身和所有子节点都需要执行的不同的卸载逻辑。例如:函数组件的useEffect
的return函数执行,ref
的解绑,class组件的componentUnmount
等逻辑处理。 - 由于
fiberNode
和dom节点不是一一对应的,所以要找到fiberNode
对应的dom节点,然后再执行删除dom节点的操作。 - 最后将删除的节点的
child
和return
指向删掉。
基于上面的2点分析,我们很容易就想到,commitDeletion
肯定会执行DFS向下遍历,进行不同子节点的删除逻辑处理。
function commitDeletion(childToDelete: FiberNode) {
let rootHostNode: FiberNode | null = null;
// 递归子树
commitNestedComponent(childToDelete, (unmountFiber) => {
switch (unmountFiber.tag) {
case HostComponent:
if (rootHostNode === null) {
rootHostNode = unmountFiber;
}
// TODO: 解绑ref
return;
case HostText:
if (rootHostNode === null) {
rootHostNode = unmountFiber;
}
return;
case FunctionComponent:
// TODO: useEffect unmount 解绑ref
return;
default:
if (__DEV__) {
console.warn("未处理的unmount类型", unmountFiber);
}
break;
}
});
// 移除rootHostNode的DOM
if (rootHostNode !== null) {
const hostParent = getHostParent(childToDelete);
if (hostParent !== null) {
removeChild((rootHostNode as FiberNode).stateNode, hostParent);
}
}
childToDelete.return = null;
childToDelete.child = null;
}
commitNestedComponent
commitNestedComponent
中主要是完成我们上面说的2点。
- DFS深度遍历子节点
- 找到当前要删除的
fiberNode
对应的真正的DOM
节点
接受2个参数。1. 当前的fiberNode
, 2. 递归到不同的子节点的同时,需要执行的回调函数执行不同的卸载流程。
function commitNestedComponent(
root: FiberNode,
onCommitUnmount: (fiber: FiberNode) => void
) {
let node = root;
while (true) {
onCommitUnmount(node);
if (node.child !== null) {
// 向下遍历
node.child.return = node;
node = node.child;
continue;
}
if (node === root) {
// 终止条件
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === root) {
return;
}
// 向上归
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
这里可能比较绕,我们下面通过几个例子总结一下,这个过程的主要流程。
总结
如果按照如下的结构,要删除外层div
元素,会经历如下的流程
<div>
<Child />
<span>hcc</span>
yx
</div>
function Child() {
return <div>hello world</div>
}
div
的fiberNode的父节的标记ChildDeletion
以及存放到deletions
中。- 当执行到
commitWork
阶段的时候,遍历deletions
数组。 - 执行的div对应的
HostComponent
, 然后执行commitDeletion
- 在
commitDeletion
中执行commitNestedComponent
向下DFS遍历。 - 在遍历的过程中,每一个节点都是执行一个回调函数,基于不同的类型执行不同的删除操作,以及记录我们要删除的Dom节点对应的fiberNode。
- 所以首先是
div
执行onCommitUnmount, 由于它是HostComponent
,所以将rootHostNode
赋值给了div
- 向下递归到
Child
节点,由于它存在子节点,继续递归到child-div
节点,继续遍历到hello world
节点。它不存在子节点。 - 然后找到
Child
的兄弟节点,以此执行,先子后兄。直到回到div
节点。
下一节预告
下一节我们讲解通过useState
改变状态后,如何更新节点以及函数组件hooks是如何保存数据的。
以上就是React18之update流程从零实现详解的详细内容,更多关于React18 update流程的资料请关注编程网其它相关文章!