likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 2

自己动手写 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 文件夹,并初始化:

cd packages
mkdir react-dom
cd react-dom
pnpm init

初始化的 package.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 文件复制过来并删除:

// 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 包里面已经实现了,直接调用即可。

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. 实现打包流程

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

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

// 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 包中的路径;

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

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

// 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,这是对我最大的鼓励和支持。