自己动手写 React源码 ——【7】实现 ReactDOM

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

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

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

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

React 是一个跨平台的库,可以用于构建 Web 应用、移动应用(React Native)等。而 react-dom 就是 React 在 Web 环境中的渲染实现,用于将 React 组件渲染到实际的 DOM 上,并提供了一些与 DOM 操作相关的功能。

之前我们在 react-reconciler/src/hostConfig.ts 中模拟实现了一些生成、插入 DOM 元素的函数,现在就在 react-dom 中真正实现它。

1. 实现 react-dom 包

先创建 packages/react-dom 文件夹,并初始化:

bash 复制代码
cd packages
mkdir react-dom
cd react-dom
pnpm init

初始化的 package.json文件如下所示:

json 复制代码
// packages/react-dom/package.json
{
	"name": "react-dom",
	"version": "1.0.0",
	"description": "",
	"module": "index.ts",
	"dependencies": {
		"shared": "workspace: *",
		"react-reconciler": "workspace: *"
	},
	"peerDependencies": {
		"react": "workspace: *"
	},
	"keywords": [],
	"author": "",
	"license": "ISC"
}

新建 packages/react-dom/scr/hostConfig.ts 文件,将之前的 hostConfig.ts 文件复制过来并删除:

typescript 复制代码
// packages/react-dom/scr/hostConfig.ts
export type Container = Element;
export type Instance = Element;

export const createInstance = (type: string, porps: any): Instance => {
	// TODO: 处理 props
	const element = document.createElement(type);
	return element;
};

export const appendInitialChild = (
	parent: Instance | Container,
	child: Instance
) => {
	parent.appendChild(child);
};

export const createTextInstance = (content: string) => {
	const element = document.createTextNode(content);
	return element;
};

export const appendChildToContainer = (
	child: Instance,
	parent: Instance | Container
) => {
	parent.appendChild(child);
};

接着实现 packages/react-dom/scr/root.ts,先来实现 ReactDOM.createRoot().render() 方法,我们之前讲过,这个函数过程中会调用两个 API:

  • createContainer 函数: 用于创建一个新的容器(container),该容器包含了 React 应用的根节点以及与之相关的一些配置信息。
  • updateContainer 函数: 用于更新已经存在的容器中的内容,将新的 React 元素(element)渲染到容器中,并更新整个应用的状态。

这两个 API 在 react-reconciler 包里面已经实现了,直接调用即可。

typescript 复制代码
import {
	createContainer,
	updateContainer
} from 'react-reconciler/src/fiberReconciler';
import { Container } from './hostConfig';
import { ReactElementType } from 'shared/ReactTypes';

// 实现 ReactDOM.createRoot(root).render(<App />);
export function createRoot(container: Container) {
	const root = createContainer(container);
	return {
		render(element: ReactElementType) {
			updateContainer(element, root);
		}
	};
}

现在我们已经实现了 React 首屏渲染的更新流程,即:

通过 ReactDOM.createRoot(root).render(<App />) 方法,创建 React 应用的根节点,将一个 Placement 加入到更新队列中,并触发了首屏渲染的更新流程:在对 Fiber 树进行深度优先遍历(DFS)的过程中,比较新旧节点,生成更新计划,执行 DOM 操作,最终将 <App /> 渲染到根节点上。

目前我们还只实现了首屏渲染触发更新,还有很多触发更新的方式,如类组件的 this.setState()、函数组件的 useState useEffect ,将在后面实现。

2. 实现打包流程

接着来实现 react-dom 包的打包流程,具体过程参考 第 2 节,需要注意两点:

  • 需要安装一个包来处理 hostConfig 的导入路径:pnpm i -D -w @rollup/plugin-alias
  • ReactDOM = Reconciler + hostConfig,不要将 react 包打包进 react-dom 里,否则会出现数据共享冲突;

react-dom.config.js 的具体配置如下:

javascript{4,6,27,30-34} 复制代码
// scripts/rollup/react-dom.config.js
import { getPackageJSON, resolvePkgPath, getBaseRollupPlugins } from './utils';
import generatePackageJson from 'rollup-plugin-generate-package-json';
import alias from '@rollup/plugin-alias';

const { name, module, peerDependencies } = getPackageJSON('react-dom');
// react-dom 包的路径
const pkgPath = resolvePkgPath(name);
// react-dom 包的产物路径
const pkgDistPath = resolvePkgPath(name, true);

export default [
	// react-dom
	{
		input: `${pkgPath}/${module}`,
		output: [
			{
				file: `${pkgDistPath}/index.js`,
				name: 'ReactDOM',
				format: 'umd'
			},
			{
				file: `${pkgDistPath}/client.js`,
				name: 'client',
				format: 'umd'
			}
		],
		external: [...Object.keys(peerDependencies)],
		plugins: [
			...getBaseRollupPlugins(),
			// webpack resolve alias
			alias({
				entries: {
					hostConfig: `${pkgPath}/src/hostConfig.ts`
				}
			}),
			generatePackageJson({
				inputFolder: pkgPath,
				outputFolder: pkgDistPath,
				baseContents: ({ name, description, version }) => ({
					name,
					description,
					version,
					peerDependencies: {
						react: version
					},
					main: 'index.js'
				})
			})
		]
	},
];

再将 tsconfig.json 中的 hostConfig 指向 react-dom 包中的路径;

json 复制代码
// tsconfig.json
{
	// ...
	"paths": {
		"hostConfig": ["./react-dom/src/hostConfig.ts"]
	}
}

最后,为了在执行 npm run build-dev 时能同时将 reactreact-dom 都打包,我们新建一个 dev.config.js 文件,将 react.config.jsreact-dom.config.js 统一导出。

javascript 复制代码
// scripts/rollup/dev.config.js
import reactDomConfig from './react-dom.config';
import reactConfig from './react.config';

export default [...reactConfig, ...reactDomConfig];

并将 package.json 中的 npm run build-dev 命令改为:"rimraf dist && rollup --config scripts/rollup/dev.config.js --bundleConfigAsCjs"

现在运行 npm run build-dev 就可以得到 reactreact-dom 的打包产物了。通过 pnpm lint --global 或者 npm run demo 可在测试项目中运行你自己开发的 react 包和 react-dom 包。


至此,我们就实现了基础版的 react-dom 包,更多的功能我们将在后面一一实现。

相关代码可在 git tag v1.7 查看,地址: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,这是对我最大的鼓励和支持。

相关推荐
酷爱码13 分钟前
css中的 vertical-align与line-height作用详解
前端·css
沐土Arvin27 分钟前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年29 分钟前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖66638 分钟前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
L耀早睡1 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer2 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿2 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹2 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹2 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年3 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net