写在前边
- 创作本篇博客的初衷是,在浏览社区时发现了https://pomb.us/build-your-own-react/这篇宝藏文章,该博主基于react16之后的fiber架构实现了一套react的简易版本,非常有助于理解react工作原理。但是苦于只有英文版本,且偏向理论。
- 本着提升自我、贡献社区的理念。在此记录下学习历程,并尽自己微薄之力对重点部分(结合自己理解)进行翻译整理。希望对大家有所帮助。
- 内容比较多,所以篇幅会较分散,大家也可以主要看自己需要的部分。我会努力更新的!
零、准备工作
- 创建项目(自己命名),下载文件包
$ mkdir xxx
$ cd xxx
$ yarn init -y / npm init -y
$ yarn add react react-dom 建立如下目录结构
src/
- myReact/
- index.js
- index.html
- main.jsx
- 初始化文件内容
//index.html
React App
// main.jsx
import React from "./React/index";
import React from "react";
import ReactDom from "react-dom";
const App = () => {
returnHello;
};
ReactDom.render(, document.getElementById("root"));
// myReact/index.js
export default {} - 安装 parcel 用于打包和热更新
$ yarn add parcel-bundler
一、createElement的功能
功不可没的babel
// main.jsx
const element = (
)
经过babel转译后的效果(使用plugin-transform-react-jsx
插件,https://www.babeljs.cn/docs/babel-plugin-transform-react-jsx#both-runtimes):
const element = React.createElement(
"div", //type
{ id: "foo" }, //config
React.createElement("a", null, "bar"), //...children
React.createElement("span")
)
- babel的
plugin-transform-react-jsx
做的事情很简单: 使用React.createElement
函数来从处理.jsx文件中的jsx语法。 - 这也就是为什么在.jsx文件中必须
import React from "react"
的原因啦,否则插件会找不到React对象的!
配置babel
tips:笔者本来也打算使用 plugin-transform-react-jsx
插件,但是在调试中遇到了问题。查找后才知道最新版本的插件已经不再是由
到 Hello World
React.createElement('h1', null, 'Hello world')
的简单转换了(具体见https://zh-hans.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html),故退而求其次选择了功能类似的 transform-jsx
$ touch .babelrc
$ yarn add babel@transform-jsx
// .babelrc
{
"presets": ["es2015"],
"plugins": [
[
"transform-jsx",
{
"function": "OllyReact.createElement",
"useVariables": true
}
]
]
}
$ parcel src/index.html
此时页面中可以看到Hello字样,说明我们配置成功了!
动手实现createElement
transform-jsx
插件会将参数封装在一个对象中,传入createElement。
// myReact/index.js
export function createElement(args) {
const { elementName, attributes, children } = args;
return {
type:elementName,
props: {
...attributes,
children
}
};
}
考虑到children中还可能包含基本类型如string,number。为了简化操作我们将这样的children统一使用 TEXT_ELEMENT
包裹。
// myReact/index.js
export function createElement(type, config, ...children) {
return {
type,
props: {
...attributes,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
}
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
export default { createElement }
React并不会像此处这样处理基本类型节点,但我们这里这样做:因为这样可以简化我们的代码。毕竟这是一篇以功能而非细节为主的文章。
看看效果
首先为我们自己的库起个名字吧!
//.babelrc
{
"presets": ["es2015"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"runtime": "automatic",
"importSource": "OllyReact"
}
]
]
}
引入时就使用自己写的名字吧!
// main.jsx
import OllyReact from "./myReact/index";
import ReactDom from "react-dom"
const element = (
Hello World
—Oliver
);
ReactDom.render(element, document.getElementById("root"));
此时页面上已经出现了Hello
, 这证明我们的React.createElement已经基本实现了React的功能。
二、Render功能
接下来编写render函数。
目前我们只关注向DOM中添加内容。修改和删除功能将在后续添加。
// React/index.js
export function render(element, container) {}
export default {
//...省略
render
};
细节实现
注意:
本小节每一步内容主要参考思路即可,详细的逻辑顺序会在底部汇总。
- 首先使用对应的元素类型创建新DOM节点,并把该DOM节点加入股container中
const dom = document.createElement(element.type)
container.appendChild(dom) - 然后递归地为每个child JSX元素执行相同的操作
element.props.children.forEach(child =>
render(child, dom)
) - 考虑到TEXT节点需要特殊处理
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type) - 最后将元素的props分配给真实DOM节点
Object.keys(element.props)
.filter(key => key !== "children") // children属性要除去。
.forEach(name => {
dom[name] = element.props[name];
});
汇总:
export function render(element, container) {
const dom = element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
Object.keys(element.props)
.filter(key => key !== "children")
.forEach(name => {
dom[name] = element.props[name];
});
element.props.children.forEach(child =>
render(child, dom)
);
container.appendChild(dom);
}
看看效果
// main.jsx
import OllyReact from "./myReact/index";
const element = (
Hello World
—Oliver
);
OllyReact.render(element, document.getElementById("root"));
此时看到我们的render函数也可以正常工作了!
小结
就是这样!现在,我们有了一个可以将JSX呈现到DOM的库(虽然它只支持原生DOM标签且不支持更新 QAQ)。