从 0 到 1 实现 React(一)实现 createElement 方法

概要

  • 本文是作者在读懂了大神Rodrigo Pombo《Build your own React》源码后,加上了自己的理解,以及做了少量修改后的实现,在这里再次感谢大神!🙏🏻
  • 核心代码 200+ 🎉
  • fiber 架构的 react 🔥
  • 通俗易懂,对标全网最简单的 react 实现 😍
  • 构建工具选用 parcel,号称零配置
  • 从零到一的实现一个 react
  • 每篇文章在最后,都会附上当前章节源码 🌐
  • github 源码

创建一个空项目

创建项目并初始化package.json

1
2
mkdir Didact
npm init -y

安装 parcel

1
npm i -D parcel-bundler@^1.12.5

为了跟着教程走不会因版本问题报错,本文接下来的所有 npm 依赖都将带上版本号

新增 index.html 模板

添加 html 代码:

  • 根节点 root
  • 内联的方式引入 一个index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Didact</title>
</head>
<body>
<div id="root"></div>
</body>
<script src="./index.js"></script>
</html>

添加入口 index.js

index.js里面是一段 JSX 代码

1
console.log('成功启动'); 

增加 script 命令

修改 package.json

1
2
3
"scripts": {
+ "start": "parcel index.html",
},

执行 npm start,访问http://localhost:1234/, 会看到控制台打印"成功启动"

如何处理 JSX

什么是 jsx?

如下就是一段 JSX 代码, 具体请参考

1
2
3
4
5
6
7
const profile = (
<div className="profile">
<span className="profile-title">title</span>
<h3 className="profile-content">content</h3>
我是一段文本
</div>
);

接下来要将上面 JSX 代码转化为下面的数据结构来描述

1
2
3
4
5
6
7
8
9
10
11
12
// 对象来描述 jsx
const profile = {
type"div",
props: {
className"profile",
children: [
{type'span'props: {…}},
{type'h3'props: {…}},
"我是一段文本"
],
},
}

安装 babel 处理 JSX

我们使用@babel/preset-react来转化 jsx,但同时需安装它所需要依赖——babel-core,所以整体安装命令如下:

1
npm i -D @babel/preset-react@^7.17.12 babel-core@^7.0.0-bridge.0

然后再根目录添加.babelrc文件:

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/preset-react",
{
// 这样写,babel会调用 Didact.createElement函数 来递归生成 jsx对象
"pragma": "Didact.createElement"
}
]
]
}

注意:我们在上面设置了pragma属性,它指定了babel通过调用Didact.createElement来递归JSX,从而生成上面的数据结构。

测试 JSX 的转换

将 index.js 的console打印改为上面的那段 JSX

1
2
3
4
5
6
7
8
9
10
11
- console.log('成功启动');

+ const profile = (
+ <div className="profile">
+ <span className="profile-title">title</span>
+ <h3 className="profile-content">content</h3>
+ 我是一段文本
+ </div>
+ );

+ console.log('profile: ', profile);

打开控制台,会看到如下错误提示:

1
2
3
4
5
Uncaught ReferenceError: Didact is not defined
at Object.parcelRequire.index.js (index.js:29:3)
at newRequire (Didact.e31bb0bc.js:47:24)
at Didact.e31bb0bc.js:81:7
at Didact.e31bb0bc.js:120:3

点击进入第一行错误定位,会跳转到源码出错的地方—— “Didact.createElement 未定义”,因为我们还未实现Didact.createElement,所以因找不到该函数而报错。

但在实现createElement方法前,我们先看看babel是如何处理jsx的。

babel转换jsx的过程

babel调用Didact.createElement转换jsx的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var profile = Didact.createElement(
// HTML 标签的类型
"div",
// 该 HTML 标签的属性
{ className: "profile" },
// 后面都该 HTML 标签的 children
// 第一个 child
Didact.createElement(
"span",
{ className: "profile-title" },
"title"
),
// 第二个 child
Didact.createElement(
"h3",
{ className: "profile-content" },
"content"
),
// 第三个 child
"我是一段文本"
);

console.log('profile: ', profile);
// ...

从上面代码可以看出,@babel/preset-react做了两件事情:

  • 将 JSX 代码转换成了参数,type, props, ...children
  • 将上面的参数传递给 Didact.createElement,并执行该函数
1
2
3
4
5
6
7
8
9
10
11
12
Didact.createElement(
type,
[props],
[...children]
)

// 参数说明:

// - type:标签类型,如:`div`、`span`、`h3`,
// 也可以是 React 组件 类型(class 组件或函数组件)
// - props: 该标签的属性,如`classname`, 若无则为 null
// - children:第 2、3...个参数,都是子元素,子元素又开始递归调用`React.createElement`

实现 createElement 方法

很简单,让Didact.createElement 返回一个含有 children 的树状结构,就实现了createElement

在index.js中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+ function createElement(type, props, ...children) {
+ return {
+ type,
+ props: {
+ ...props,
+ ...children,
+ }
+ };
+ }

+ const Didact = {
+ createElement,
+ };

const profile = (
<div className="profile">
<span className="profile-title">title</span>
<h3 className="profile-content">content</h3>
我是一段文本
</div>
);

console.log('profile: ', profile);

这样就实现了createElement方法。

但通过console打印发现,children中的所有元素,除了文本节点string其它节点都是对象

这里将文本节点也统一处理成对象,这样后面会少了很多if、else的判断。

将文本节点构建为type:'TEXT_ELEMENT'的对象,修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
- ...children,
+ children: children.map(child =>
+ typeof child === "object" ? child : createTextElement(child)
+ )
}
};
}

+ function createTextElement(text) {
+ return {
+ type: "TEXT_ELEMENT",
+ props: {
+ nodeValue: text,
+ children: []
+ }
+ };
+ }

const Didact = {
createElement,
};

这样我们就彻底完成了createElement方法

本章源码