自己动手写 React源码 ——【5】实现 Render 阶段

深入理解 React 源码,带你从零实现 React v18 的核心功能,构建自己的 React 库。

电子书地址:2xiao.github.io/leetcode-js...

源代码地址:github.com/2xiao/my-re...

送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。

上一节 ,我们探讨了 React 应用在首次渲染或后续更新时的整体更新流程。在 Reconciler 工作流程中,beginWorkcompleteWork 两个方法起到了关键作用。beginWork 负责构建表示更新的 Fiber 树,而 completeWork 则将这个 Fiber 树映射到实际的 DOM 结构上。接下来,我们将深入实现这两个方法。

1. 开发环境打印日志

首先,在开发环境下增加 __DEV__ 标识,以便在开发环境中方便地打印日志:

bash 复制代码
pnpm i -D -w @rollup/plugin-replace

安装完成后,在 scripts/rollup/utils.js 中引入:

javascript 复制代码
// scripts/rollup/utils.js
// ...
import replace from '@rollup/plugin-replace';

// ...

export function getBaseRollupPlugins({
	alias = { __DEV__: true },
	typescript = {}
} = {}) {
	return [replace(alias), cjs(), ts(typescript)];
}

这样我们就可以在开发环境中打印日志了。

2. 实现 beginWork

beginWork 函数在向下遍历阶段执行,根据 Fiber 节点的类型(HostRootHostComponentHostText)分发任务给不同的处理函数,处理具体节点类型的更新逻辑:

  • HostRoot

    • 表示根节点,即应用的最顶层节点;
    • 调用 updateHostRoot 函数,处理根节点的更新,包括协调处理根节点的属性以及子节点的更新逻辑;
    • 调用 reconcileChildren 函数,处理根节点的子节点,可能会递归调用其他协调函数;
    • 返回 workInProgress.child 表示经过协调后的新的子节点链表;
  • HostComponent

    • 表示原生 DOM 元素节点,例如 <div><span> 等;
    • 调用 updateHostComponent 函数,处理原生 DOM 元素节点的更新,负责协调处理属性和子节点的更新逻辑;
    • 调用 reconcileChildren 函数,处理原生 DOM 元素的子节点更新;
    • 返回 workInProgress.child 表示经过协调后的新的子节点链表;
  • HostText

    • 表示文本节点,即 DOM 中的文本内容,例如 <p>123</p> 中的 123
    • 调用 updateHostText 函数,协调处理文本节点的内容更新;
    • 返回 null 表示已经是叶子节点,没有子节点了;

其中 reconcileChildren 函数的作用是,通过对比子节点的 current FiberNode 与 子节点的 ReactElement,来生成子节点对应的 workInProgress FiberNode。(current 是与视图中真实 UI 对应的 Fiber 树,workInProgress 是触发更新后正在 Reconciler 中计算的 Fiber 树。)

typescript 复制代码
// packages/react-reconciler/src/beginWork.ts
import { ReactElementType } from 'shared/ReactTypes';
import { FiberNode } from './fiber';
import { UpdateQueue, processUpdateQueue } from './updateQueue';
import { HostComponent, HostRoot, HostText } from './workTags';
import { reconcileChildFibers, mountChildFibers } from './childFiber';

// 比较并返回子 FiberNode
export const beginWork = (workInProgress: FiberNode) => {
	switch (workInProgress.tag) {
		case HostRoot:
			return updateHostRoot(workInProgress);
		case HostComponent:
			return updateHostComponent(workInProgress);
		case HostText:
			return updateHostText();
		default:
			if (__DEV__) {
				console.warn('beginWork 未实现的类型', workInProgress.tag);
			}
			break;
	}
};

function updateHostRoot(workInProgress: FiberNode) {
	// 根据当前节点和工作中节点的状态进行比较,处理属性等更新逻辑
	const baseState = workInProgress.memoizedState;
	const updateQueue = workInProgress.updateQueue as UpdateQueue<Element>;
	const pending = updateQueue.shared.pending;
	// 清空更新链表
	updateQueue.shared.pending = null;
	// 计算待更新状态的最新值
	const { memoizedState } = processUpdateQueue(baseState, pending);
	workInProgress.memoizedState = memoizedState;

	// 处理子节点的更新逻辑
	const nextChildren = workInProgress.memoizedState;
	reconcileChildren(workInProgress, nextChildren);

	// 返回新的子节点
	return workInProgress.child;
}

function updateHostComponent(workInProgress: FiberNode) {
	const nextProps = workInProgress.pendingProps;
	const nextChildren = nextProps.children;
	reconcileChildren(workInProgress, nextChildren);
	return workInProgress.child;
}

function updateHostText() {
	// 没有子节点,直接返回 null
	return null;
}

// 对比子节点的 current FiberNode 与 子节点的 ReactElement
// 生成子节点对应的 workInProgress FiberNode
function reconcileChildren(
	workInProgress: FiberNode,
	children?: ReactElementType
) {
	// alternate 指向节点的备份节点,即 current
	const current = workInProgress.alternate;
	if (current !== null) {
		// 组件的更新阶段
		workInProgress.child = reconcileChildFibers(
			workInProgress,
			current?.child,
			children
		);
	} else {
		// 首屏渲染阶段
		workInProgress.child = mountChildFibers(workInProgress, null, children);
	}
}

reconcileChildren 函数中调用了 reconcileChildFibersmountChildFibers 两个函数,它们分别负责处理更新阶段和首次渲染阶段的子节点协调。

  • reconcileChildFibers:

    • reconcileChildFibers 函数作用于组件的更新阶段,即当组件已经存在于 DOM 中,需要进行更新时。
    • 主要任务是协调处理当前组件实例的子节点,对比之前的子节点(current)和新的子节点(workInProgress)之间的变化。
    • 根据子节点的类型和 key 进行对比,决定是复用、更新、插入还是删除子节点,最终形成新的子节点链表。
  • mountChildFibers:

    • mountChildFibers 函数作用于组件的首次渲染阶段,即当组件第一次被渲染到 DOM 中时。
    • 主要任务是协调处理首次渲染时组件实例的子节点。
    • 但此时是首次渲染,没有之前的子节点,所以主要是创建新的子节点链表。
typescript 复制代码
// packages/react-reconciler/src/childFiber.ts
import { ReactElementType } from 'shared/ReactTypes';
import { FiberNode, createFiberFromElement } from './fiber';
import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols';
import { HostText } from './workTags';
import { Placement } from './fiberFlags';

function ChildReconciler(shouldTrackSideEffects: boolean) {
	// 处理单个 Element 节点的情况
	// 对比 currentFiber 与 ReactElement
	// 生成 workInProgress FiberNode
	function reconcileSingleElement(
		returnFiber: FiberNode,
		currentFiber: FiberNode | null,
		element: ReactElementType
	) {
		const fiber = createFiberFromElement(element);
		fiber.return = returnFiber;
		return fiber;
	}

	// 处理文本节点的情况
	// 对比 currentFiber 与 ReactElement
	// 生成 workInProgress FiberNode
	function reconcileSingleTextNode(
		returnFiber: FiberNode,
		currentFiber: FiberNode | null,
		content: string | number
	) {
		const fiber = new FiberNode(HostText, { content }, null);
		fiber.return = returnFiber;
		return fiber;
	}

	// 为 Fiber 节点添加更新 flags
	function placeSingleChild(fiber: FiberNode) {
		// 首屏渲染且追踪副作用时,才添加更新 flags
		if (shouldTrackSideEffects && fiber.alternate == null) {
			fiber.flags |= Placement;
		}
		return fiber;
	}

	// 闭包,根绝 shouldTrackSideEffects 返回不同 reconcileChildFibers 的实现
	return function reconcileChildFibers(
		returnFiber: FiberNode,
		currentFiber: FiberNode | null,
		newChild?: ReactElementType
	) {
		// 判断当前 fiber 的类型
		// 单个 Element 节点
		if (typeof newChild == 'object' && newChild !== null) {
			switch (newChild.$$typeof) {
				case REACT_ELEMENT_TYPE:
					return placeSingleChild(
						reconcileSingleElement(returnFiber, currentFiber, newChild)
					);

				default:
					if (__DEV__) {
						console.warn('未实现的 reconcile 类型', newChild);
					}
					break;
			}
		}

		// 多个 Element 节点
		if (Array.isArray(newChild)) {
			// TODO: 暂时不处理
			if (__DEV__) {
				console.warn('未实现的 reconcile 类型', newChild);
			}
		}

		// 文本节点
		if (typeof newChild == 'string' || typeof newChild == 'number') {
			return placeSingleChild(
				reconcileSingleTextNode(returnFiber, currentFiber, newChild)
			);
		}

		if (__DEV__) {
			console.warn('未实现的 reconcile 类型', newChild);
		}
		return null;
	};
}

// 组件的更新阶段中,追踪副作用
export const reconcileChildFibers = ChildReconciler(true);

// 首屏渲染阶段中不追踪副作用,只对根节点执行一次 DOM 插入操作
export const mountChildFibers = ChildReconciler(false);
typescript 复制代码
// packages/react-reconciler/src/fiber.ts
// ...

// 根据 DOM 节点创建新的 Fiber 节点
export function createFiberFromElement(element: ReactElementType): FiberNode {
	const { type, key, props } = element;
	let fiberTag: WorkTag = FunctionComponent;
	if (typeof type == 'string') {
		// 如: <div/> 的 type: 'div'
		fiberTag = HostComponent;
	} else if (typeof type !== 'function' && __DEV__) {
		console.warn('未定义的 type 类型', element);
	}

	const fiber = new FiberNode(fiberTag, props, key);
	fiber.type = type;
	return fiber;
}

3. 实现 completeWork

completeWork 函数在向上遍历阶段执行,根据 Fiber 节点的类型(HostRootHostComponentHostText 等)构建 DOM 节点,收集更新 flags,并根据更新 flags 执行不同的 DOM 操作:

  • HostComponent:

    • 表示原生 DOM 元素节点;
    • 构建 DOM 节点,并调用 appendAllChildren 函数将 DOM 插入到 DOM 树中;
    • 收集更新 flags,并根据更新 flags 执行不同的 DOM 操作,例如插入新节点、更新节点属性、删除节点等;
  • HostText:

    • 表示文本节点;
    • 构建 DOM 节点,并将 DOM 插入到 DOM 树中;
    • 收集更新 flags,根据 flags 的值,更新文本节点的内容;
  • HostRoot:

    • 表示根节点;
    • 会执行一些与根节点相关的最终操作,例如处理根节点的属性,确保整个应用更新完毕;
typescript 复制代码
// packages/react-reconciler/src/completeWork.ts
import {
	appendInitialChild,
	createInstance,
	createTextInstance
} from 'hostConfig';
import { FiberNode } from './fiber';
import { HostComponent, HostRoot, HostText } from './workTags';
import { NoFlags } from './fiberFlags';

// 生成更新计划,计算和收集更新 flags
export const completeWork = (workInProgress: FiberNode) => {
	const newProps = workInProgress.pendingProps;
	const current = workInProgress.alternate;
	switch (workInProgress.tag) {
		case HostRoot:
			bubbleProperties(workInProgress);
			return null;

		case HostComponent:
			if (current !== null && workInProgress.stateNode !== null) {
				// TODO: 组件的更新阶段
			} else {
				// 首屏渲染阶段
				// 构建 DOM
				const instance = createInstance(workInProgress.type, newProps);
				// 将 DOM 插入到 DOM 树中
				appendAllChildren(instance, workInProgress);
				workInProgress.stateNode = instance;
			}
			// 收集更新 flags
			bubbleProperties(workInProgress);
			return null;

		case HostText:
			if (current !== null && workInProgress.stateNode !== null) {
				// TODO: 组件的更新阶段
			} else {
				// 首屏渲染阶段
				// 构建 DOM
				const instance = createTextInstance(newProps.content);
				workInProgress.stateNode = instance;
			}
			// 收集更新 flags
			bubbleProperties(workInProgress);
			return null;

		default:
			if (__DEV__) {
				console.warn('completeWork 未实现的类型', workInProgress);
			}
			return null;
	}
};

其中,appendAllChildren 函数负责递归地将组件的子节点添加到指定的 parent 中,它通过深度优先遍历 workInProgress 的子节点链表,处理每个子节点的类型。先处理当前节点的所有子节点,再处理兄弟节点。

如果它是原生 DOM 元素节点或文本节点,则将其添加到父节点中;如果是其他类型的组件节点并且有子节点,则递归处理其子节点。

typescript 复制代码
// packages/react-reconciler/src/completeWork.ts
// ...

function appendAllChildren(
	parent: Container | Instance,
	workInProgress: FiberNode
) {
	let node = workInProgress.child;
	while (node !== null) {
		if (node.tag == HostComponent || node.tag == HostText) {
			// 处理原生 DOM 元素节点或文本节点
			appendInitialChild(parent, node.stateNode);
		} else if (node.child !== null) {
			// 递归处理其他类型的组件节点的子节点
			node.child.return = node;
			node = node.child;
			continue;
		}
		if (node == workInProgress) {
			return;
		}

		while (node.sibling === null) {
			if (node.return === null || node.return === workInProgress) {
				return;
			}
			node = node.return;
		}
		// 处理下一个兄弟节点
		node.sibling.return = node.return;
		node = node.sibling;
	}
}

bubbleProperties 函数则负责在 completeWork 函数向上遍历的过程中,通过向上冒泡子节点的 flags,将所有更新 flags 收集到根节点。主要包含以下步骤:

  • 从当前需要冒泡属性的 Fiber 节点开始,检查是否有需要冒泡的属性。
  • 如果当前节点有需要冒泡的属性,将这些属性冒泡到父节点的 subtreeFlags 或其他适当的属性中。
  • 递归调用 bubbleProperties 函数,处理父节点,将属性继续冒泡到更上层的祖先节点,直至达到根节点。
typescript 复制代码
// packages/react-reconciler/src/completeWork.ts
// ...

// 收集更新 flags,将子 FiberNode 的 flags 冒泡到父 FiberNode 上
function bubbleProperties(workInProgress: FiberNode) {
	let subtreeFlags = NoFlags;
	let child = workInProgress.child;
	while (child !== null) {
		subtreeFlags |= child.subtreeFlags;
		subtreeFlags |= child.flags;

		child.return = workInProgress;
		child = child.sibling;
	}

	workInProgress.subtreeFlags |= subtreeFlags;
}

4. 位运算简介

flags 是 React 中很重要的一环,具体作用是通过二进制在每个 Fiber 节点保存其本身与子节点的 flags。在保存与处理 flags 时,使用了一些二进制运算符,我们来复习一下:

1. | 运算

| 运算的两个位都为 0 时,结果才为 0:

  • 1 | 1 = 1
  • 1 | 0 = 1
  • 0 | 0 = 0

React 利用了 | 运算符的特性来存储 flags,如:

ts 复制代码
const NoFlags = /*            */ 0b0000000;
const PerformedWork = /*      */ 0b0000001;
const Placement = /*          */ 0b0000010;
const Update = /*             */ 0b0000100;
const ChildDeletion = /*      */ 0b0001000;

const flags = Placement | Update; //此时 flags = 0b0000110

2. & 运算

& 运算的两个位都为 1 时,结果才为 1:

  • 1 & 1 = 1
  • 1 & 0 = 0
  • 0 & 0 = 0

React 中会用一个 flags & 某一个 flag,来判断 flags 中是否包含某一个 flag,如:

ts 复制代码
const flags = Placement | Update; //此时 flags = 0b0000110

Boolean(flags & Placement); // true, 说明 flags 中包含 Placement
Boolean(flags & ChildDeletion); // false, 说明 flags 中不包含 ChildDeletion

3. ~ 运算

运算符会把每一位取反,0 变 1,1 变 0:

  • ~1 = 0
  • ~0 = 1

在 React 中,~ 运算符同样是常用操作,如:

ts 复制代码
let flags = Placement | Update; //此时 flags = 0b0000110

flags &= ~Placement; //此时 flags = 0b0000100

通过 ~ 运算符与 & 运算符的结合,从 flags 中删除了 Placement 这个 flag。


至此,我们就实现了 React 协调阶段中的 beginWorkcompleteWork 函数,生成了一棵表示更新的 Fiber 树,并收集了树中节点的更新 flags,下一节我们将根据这些 flags 执行对应的 DOM 操作。

相关代码可在 git tag v1.5 查看,地址:github.com/2xiao/my-re...


《自己动手写 React 源码》遵循 React 源码的核心思想,通俗易懂的解析 React 源码,带你从零实现 React v18 的核心功能。

学完本书,你将有这些收获:

  • 面试加分:框架底层原理是面试必问环节,熟悉 React 源码会为你的面试加分,也会为你拿下 offer 增加不少筹码;

  • 提升开发效率:熟悉 React 源码之后,会对 React 的运行流程有新的认识,让你在日常的开发中,对性能优化、使用技巧和 bug 解决更加得心应手;

  • 巩固基础知识:学习本书也顺便巩固了数据结构和算法,如 reconciler 中使用了 fiber、update、链表等数据结构,diff 算法要考虑怎样降低对比复杂度;

本书的特色:

  • 教程详细,代码开源,带你构建自己的 React 库;

  • 功能全面,可跑通官方测试用例;

  • 按 Git Tag 划分迭代步骤,记录每个功能的实现过程;

电子书地址:2xiao.github.io/leetcode-js...

源代码地址:github.com/2xiao/my-re...

送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。

相关推荐
mldong4 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排5 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60615 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰7 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi8 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen9 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马9 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒10 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端