深入理解 React 源码,带你从零实现 React v18 的核心功能,构建自己的 React 库。
电子书地址:https://2xiao.github.io/leetcode-js/react
源代码地址:https://github.com/2xiao/my-react
送我一个免费的 ⭐️ 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. 实现打包流程
接着来实现 react-dom
包的打包流程,具体过程参考 第 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
时能同时将 react
和 react-dom
都打包,我们新建一个 dev.config.js
文件,将 react.config.js
和 react-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
就可以得到 react
和 react-dom
的打包产物了。通过 pnpm lint --global
或者 npm run demo
可在测试项目中运行你自己开发的 react
包和 react-dom
包。
至此,我们就实现了基础版的 react-dom
包,更多的功能我们将在后面一一实现。
相关代码可在 git tag v1.7
查看,地址:https://github.com/2xiao/my-react/tree/v1.7
《自己动手写 React 源码》遵循 React 源码的核心思想,通俗易懂的解析 React 源码,带你从零实现 React v18 的核心功能。
学完本书,你将有这些收获:
- 面试加分:框架底层原理是面试必问环节,熟悉 React 源码会为你的面试加分,也会为你拿下 offer 增加不少筹码;
- 提升开发效率:熟悉 React 源码之后,会对 React 的运行流程有新的认识,让你在日常的开发中,对性能优化、使用技巧和 bug 解决更加得心应手;
- 巩固基础知识:学习本书也顺便巩固了数据结构和算法,如 reconciler 中使用了 fiber、update、链表等数据结构,diff 算法要考虑怎样降低对比复杂度;
本书的特色:
- 教程详细,代码开源,带你构建自己的 React 库;
- 功能全面,可跑通官方测试用例;
- 按 Git Tag 划分迭代步骤,记录每个功能的实现过程;
电子书地址:https://2xiao.github.io/leetcode-js/react
源代码地址:https://github.com/2xiao/my-react
送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。