200+ 行代码实现一个 fiber 架构的 react 🍉 (一)实现 createElement 方法

992 阅读3分钟

概要

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

创建一个空项目

创建项目并初始化package.json

mkdir Didact
npm init -y 

安装 parcel

npm i -D parcel-bundler@^1.12.5

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

新增 index.html 模板

添加 html 代码:

  • 根节点 root
  • 内联的方式引入 一个index.js
<!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 代码

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

增加 script 命令

修改 package.json

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

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

如何处理 JSX

什么是 jsx?

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

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

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

// 对象来描述jsx
const profile = {
    type"div",
    props: {
       className"profile",
       children: [
           {type'span'props: {…}},
           {type'h3'props: {…}},
           "我是一段文本"
       ],
   },
}

安装 babel 处理 JSX

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

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

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

{
  "presets": [
    [
      "@babel/preset-react",
      {
        // 这样写,babel会调用 Didact.createElement函数 来递归生成 jsx对象
        "pragma": "Didact.createElement"
      }
    ]
  ]
}

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

测试 JSX 的转换

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

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

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

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

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

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的过程如下:

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,并执行该函数
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中添加:

+ 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'的对象,修改代码:

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方法

本章源码