1
头图

深入理解 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 时能同时将 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 查看,地址: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,这是对我最大的鼓励和支持。


TNTWEB
3.8k 声望8.5k 粉丝

腾讯新闻前端团队