Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

《自己动手写 React》——【10】实现单节点 update #10

Open
2xiao opened this issue Mar 24, 2024 · 0 comments
Open

《自己动手写 React》——【10】实现单节点 update #10

2xiao opened this issue Mar 24, 2024 · 0 comments

Comments

@2xiao
Copy link
Owner

2xiao commented Mar 24, 2024

第 8 节中,我们已经完成了组件首次渲染时的 useState 方法,现在我们将继续实现更新时的 useState 方法。

在更新流程和首次渲染流程中,存在一些关键区别:

  • 对于 beginWork 阶段:
    • 需要处理节点删除的情况(ChildDeletion);
    • 需要处理节点移动的情况(例如从 abc 变为 bca);
  • 对于 completeWork 阶段:
    • 需要处理 HostText 内容的更新情况;
    • 需要处理 HostComponent 属性的变化情况;
  • 对于 commitWork 阶段:
    • 处理 Update flags;
    • 处理 ChildDeletion flags,遍历被删除的子树;
  • 对于 useState 方法:
    • 需要实现与 mountState 相对应的 updateState 方法;

1. 处理 beginWork 阶段

在本节中,我们仅处理单一节点的情况,先跳过多节点的情况,因此也不需要考虑节点移动的情况。具体的处理流程如下:

  • 首先,我们需要比较是否可以复用当前的 Fiber 节点。

    • 首先比较节点的 key,如果 key 不同,表示不能复用。
    • 如果 key 相同,则继续比较节点的 type,如果 type 不同,同样不能复用。
    • 如果 keytype 都相同,表示可以复用。
  • 如果不能复用当前的 Fiber 节点,则需要标记删除当前的 Fiber 节点,并创建一个新的 Fiber 节点。

  • 如果可以复用,就直接复用旧的 Fiber 节点。

// 处理单个 Element 节点的情况
// 对比 currentFiber 与 ReactElement,生成 workInProgress FiberNode
function reconcileSingleElement(
	returnFiber: FiberNode,
	currentFiber: FiberNode | null,
	element: ReactElementType
) {
	// 组件的更新阶段
	if (currentFiber !== null) {
		if (currentFiber.key === element.key) {
			if (element.$$typeof === REACT_ELEMENT_TYPE) {
				if (currentFiber.type === element.type) {
					// key 和 type 都相同,复用旧的 Fiber 节点
					const existing = useFiber(currentFiber, element.props);
					existing.return = returnFiber;
					return existing;
				}
				// key 相同,但 type 不同,删除旧的 Fiber 节点
				deleteChild(returnFiber, currentFiber);
			} else {
				if (__DEV__) {
					console.warn('还未实现的 React 类型', element);
				}
			}
		} else {
			// key 不同,删除旧的 Fiber 节点
			deleteChild(returnFiber, currentFiber);
		}
	}
	// 创建新的 Fiber 节点
	const fiber = createFiberFromElement(element);
	fiber.return = returnFiber;
	return fiber;
}

// 处理文本节点的情况
// 对比 currentFiber 与 ReactElement,生成 workInProgress FiberNode
function reconcileSingleTextNode(
	returnFiber: FiberNode,
	currentFiber: FiberNode | null,
	content: string | number
) {
	if (currentFiber !== null) {
		// 组件的更新阶段
		if (currentFiber.tag === HostText) {
			// 复用旧的 Fiber 节点
			const existing = useFiber(currentFiber, { content });
			existing.return = returnFiber;
			return existing;
		} else {
			// 删除旧的 Fiber 节点
			deleteChild(returnFiber, currentFiber);
		}
	}
	// 创建新的 Fiber 节点
	const fiber = new FiberNode(HostText, { content }, null);
	fiber.return = returnFiber;
	return fiber;
}

useFiber 函数中,我们实现了复用旧的 Fiber 节点的功能。需要注意的是,对于同一个 Fiber 节点,在多次更新中,currentworkInProgress 这两个 Fiber 节点会被反复重用。

这是因为在 React 中,每个 Fiber 节点都有一个 alternate 指针,指向其在上一次渲染中对应的 Fiber 节点。在 createWorkInProgress 函数中,我们通过 current.alternate 指针获取了上一次渲染中对应的 Fiber 节点 workInProgress,并且返回了经过处理后的 workInProgress,这种重用机制有助于减少内存消耗和提高性能。

// 复用 Fiber 节点
function useFiber(fiber: FiberNode, pendingProps: Props): FiberNode {
	const clone = createWorkInProgress(fiber, pendingProps);
	clone.index = 0;
	clone.sibling = null;
	return clone;
}

deleteChild 函数中,我们实现了删除旧的 Fiber 节点的功能。具体来说,就是将旧的 Fiber 节点加入到其父节点的 deletions 参数中,并为其父节点增加 ChildDeletion flags 标记。

deletions 参数是一个数组,用于记录需要被删除的节点,然后在适当的时机,React 会遍历 deletions 数组,执行相应节点的删除操作。

// 从父节点中删除指定的子节点
function deleteChild(returnFiber: FiberNode, childToDelete: FiberNode): void {
	if (!shouldTrackSideEffects) {
		return;
	}
	const deletions = returnFiber.deletions;
	if (deletions === null) {
		returnFiber.deletions = [childToDelete];
		returnFiber.flags |= ChildDeletion;
	} else {
		deletions.push(childToDelete);
	}
}

2. 处理 completeWork 阶段

completeWork 阶段,会根据 Fiber 节点的类型(HostRootHostComponentHostText 等)构建 DOM 节点,收集更新 flags,并根据更新 flags 执行不同的 DOM 操作。

之前我们已经实现首屏渲染时的 completeWork 函数,现在只需要在其中增加组件更新的情况处理,分别是:

  • 处理 HostComponent 属性的变化情况;
  • 处理 HostText 内容的更新情况;
// 生成更新计划,计算和收集更新 flags
export const completeWork = (workInProgress: FiberNode) => {
	const newProps = workInProgress.pendingProps;
	const current = workInProgress.alternate;
	switch (workInProgress.tag) {
		// ...

		case HostComponent:
			if (current !== null && workInProgress.stateNode != null) {
				// 组件的更新阶段
				updateHostComponent(current, workInProgress);
			}
		// ...

		case HostText:
			if (current !== null && workInProgress.stateNode !== null) {
				// 组件的更新阶段
				updateHostText(current, workInProgress);
			}
		// ...
	}
};

function updateHostText(current: FiberNode, workInProgress: FiberNode) {
	const oldText = current.memoizedProps.content;
	const newText = workInProgress.pendingProps.content;
	if (oldText !== newText) {
		markUpdate(workInProgress);
	}
}

function updateHostComponent(current: FiberNode, workInProgress: FiberNode) {
	markUpdate(workInProgress);
}

// 为 Fiber 节点增加 Update flags
function markUpdate(workInProgress: FiberNode) {
	workInProgress.flags |= Update;
}

3. 处理 commitWork 阶段

commitWork 阶段,会深度优先遍历 Fiber 树,递归地向下寻找子节点是否存在需要执行的 flags,而 commitMutationEffectsOnFiber 函数会根据每个节点的 flags 和更新计划中的信息执行相应的 DOM 操作。

因此我们需要在 commitMutationEffectsOnFiber 函数中增加对 UpdateChildDeletion flags 的处理。

// packages/react-reconciler/src/commitWork.ts
const commitMutationEffectsOnFiber = (finishedWork: FiberNode) => {
	const flags = finishedWork.flags;
	if ((flags & Placement) !== NoFlags) {
		commitPlacement(finishedWork);
		// 处理完之后,从 flags 中删除 Placement 标记
		finishedWork.flags &= ~Placement;
	}
	if ((flags & ChildDeletion) !== NoFlags) {
		const deletions = finishedWork.deletions;
		if (deletions !== null) {
			deletions.forEach((childToDelete) => {
				commitDeletion(childToDelete);
			});
		}
		finishedWork.flags &= ~ChildDeletion;
	}
	if ((flags & Update) !== NoFlags) {
		commitUpdate(finishedWork);
		finishedWork.flags &= ~Update;
	}
};

若 Fiber 节点包含 Update flags,需要更新相应的 DOM 节点,先只处理节点为 HostText 类型的情况:

// packages/react-dom/src/hostConfig.ts
export const commitUpdate = (fiber: FiberNode) => {
	switch (fiber.tag) {
		case HostComponent:
			// TODO
			break;
		case HostText:
			const text = fiber.memoizedProps.content;
			commitTextUpdate(fiber.stateNode, text);
			break;
		default:
			if (__DEV__) {
				console.warn('未实现的 commitUpdate 类型', fiber);
			}
	}
};

export const commitTextUpdate = (
	textInstance: TextInstance,
	content: string
) => {
	textInstance.textContent = content;
};

若 Fiber 节点包含 ChildDeletion flags,不仅需要删除该节点及其子树,还需要对子树进行如下处理:

  • 对于 FunctionComponent,需要处理 useEffect unmount,解绑 ref;
  • 对于 HostComponent,需要解绑 ref;
  • 对于子树的「根 HostComponent」,需要移除 DOM;
// packages/react-reconciler/src/commitWork.ts

// 删除节点及其子树
const commitDeletion = (childToDelete: FiberNode) => {
	if (__DEV__) {
		console.log('执行 Deletion 操作', childToDelete);
	}

	// 子树的根节点
	let rootHostNode: FiberNode | null = null;

	// 递归遍历子树
	commitNestedUnmounts(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
				return;
			default:
				if (__DEV__) {
					console.warn('未实现的 delete 类型', unmountFiber);
				}
		}
	});

	// 移除 rootHostNode 的DOM
	if (rootHostNode !== null) {
		// 找到待删除子树的根节点的 parent DOM
		const hostParent = getHostParent(childToDelete) as Container;
		removeChild((rootHostNode as FiberNode).stateNode, hostParent);
	}

	childToDelete.return = null;
	childToDelete.child = null;
};

// 深度优先遍历 Fiber 树,执行 onCommitUnmount
const commitNestedUnmounts = (
	root: FiberNode,
	onCommitUnmount: (unmountFiber: 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;
	}
};
// packages/react-dom/src/hostConfig.ts
export const removeChild = (
	child: Instance | TextInstance,
	container: Container
) => {
	container.removeChild(child);
};

4. 处理 useState 方法

之前我们实现了在首屏渲染阶段被调用的 Hooks 集合: HooksDispatcherOnMount,现在就来实现组件更新阶段调用的 Hooks 集合 HooksDispatcherOnUpdate

// packages/react-reconciler/src/fiberHooks.ts
const HooksDispatcherOnUpdate: Dispatcher = {
	useState: updateState
};

function updateState<State>(): [State, Dispatch<State>] {
	if (__DEV__) {
		console.log('updateState 开始');
	}
	// 当前正在工作的 useState
	const hook = updateWorkInProgressHook();

	// 计算新 state 的逻辑
	const queue = hook.queue as UpdateQueue<State>;
	const pending = queue.shared.pending;

	if (pending !== null) {
		const { memoizedState } = processUpdateQueue(hook.memoizedState, pending);
		hook.memoizedState = memoizedState;
	}
	return [hook.memoizedState, queue.dispatch as Dispatch<State>];
}

updateState 中通过 updateWorkInProgressHook 函数来找到当前正在工作的 useState,从 current 中获取 Hook 的数据:

// packages/react-reconciler/src/fiberHooks.ts
function updateWorkInProgressHook(): Hook {
	// TODO render 阶段触发的更新
	// 保存链表中的下一个 Hook
	let nextCurrentHook: Hook | null;
	if (currentHook == null) {
		// 这是函数组件 update 时的第一个 hook
		let current = (currentlyRenderingFiber as FiberNode).alternate;
		if (current === null) {
			nextCurrentHook = null;
		} else {
			nextCurrentHook = current.memoizedState;
		}
	} else {
		// 这是函数组件 update 时后续的 hook
		nextCurrentHook = currentHook.next;
	}

	if (nextCurrentHook == null) {
		throw new Error(
			`组件 ${currentlyRenderingFiber?.type} 本次执行时的 Hooks 比上次执行多`
		);
	}

	currentHook = nextCurrentHook as Hook;
	const newHook: Hook = {
		memoizedState: currentHook.memoizedState,
		queue: currentHook.queue,
		next: null
	};
	if (workInProgressHook == null) {
		// update 时的第一个hook
		if (currentlyRenderingFiber !== null) {
			workInProgressHook = newHook;
			currentlyRenderingFiber.memoizedState = workInProgressHook;
		} else {
			// currentlyRenderingFiber == null 代表 Hook 执行的上下文不是一个函数组件
			throw new Error('Hooks 只能在函数组件中执行');
		}
	} else {
		// update 时的其他 hook
		// 将当前处理的 Hook.next 指向新建的 hook,形成 Hooks 链表
		workInProgressHook.next = newHook;
		// 更新当前处理的 Hook
		workInProgressHook = newHook;
	}
	return workInProgressHook;
}

至此,我们就实现了单节点的更新流程。

相关代码可在 git tag v1.10 查看,地址:https://github.com/2xiao/my-react/tree/v1.10

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant