‘Hi, my name is, what?‘,
‘My name is, who?‘,
‘My name is, chka-chka’,
‘Slim React! 🤣’
月更计划 2/1
每每看到好兄弟们的内推、平台上的职位要求,大都是“深入了解框架原理”之类的要求,苦于自己作为一个菜鸟,看源码是不可能看源码的,先不说看不看的懂,甚至也看不下去;恰好 @阿崔cxr 大佬拉着一群哥们搞了个 7 天打卡手写一个简单的 Mini-React 的活动,本着学点,反正学不进多少的态度,就跟着一步步写完了这个小项目。鉴于这个 Mini-React 相比 @阿崔cxr 大佬之前做的 Mini-Vue 项目,实在是 mini 太多,我更愿意称我的这个项目为 Slim-React 🤪。
Slim-React 的仓库地址: https://github.com/Nauxscript/slim-react,如果有疑问的可以查阅代码或者留下你的 issue,哪怕是动动发财的手指点个不要钱的 ⭐ 和 follow 一下呢~
那直入今天正题:
“如何用 200 行代码实现一个超瘦的 React ?”
简单组件渲染
首先,去 React 官网看看文档最简单使用方式;其示例代码如下:
import { createRoot } from 'react-dom/client';
// Clear the existing HTML content
document.body.innerHTML = '<div id="app"></div>';
// Render your React component instead
const root = createRoot(document.getElementById('app'));
root.render(<h1>Hello, world</h1>);
可以看到,其大概意思是:需要指定一个页面的已有的节点(以上的 <div id="app"></div>)作为 React 挂载的根容器。创建 React 的根容器使用 createRoot ,挂载使用 root.render。
那对于我们来说,第一步首先是创建一个叫做 createRoot 的方法,这个方法接收一个真实节点,返回一个内部带有 render 方法的对象,而 render 接收的是一个 JSX 组件。
大手一挥:
function createRoot(container) {
return {
render(App) {
//...
}
}
}
好,很好,这个只有皮包骨的 React 已经实现了👍 距离目标还远着呢。
这里就有个大问题:如何渲染 JSX 组件?
遇到问题的时候,我们可以暂且简化问题,然后简单实现;对于 JSX 组件来说,我们可以看看它的本质是什么。打开 React Playground ,在代码声明一个简单组件并输出到控制台查看它的结构:

可以看到,最终 JSX 组件在代码中只是一个对象,看起来是对元素的一个抽象描述。根据 React 文档中对 React Element 的描述: React Element 是对用户界面的一部分的轻量级描述。而 React Element 可以通过 React 的 createElement 方法进行创建。我们可以根据文档尝试在 React Playground 中创建 <h1>what is JSX Component ?</h1> 对应的 React Element,看看返回的数据的结构是怎么样的:

可以看出,这和上面的 JSX 组件输出的结果是一样的。所以我们可以先传递一个 render 接收的参数认为是一个 vdom 对象。声明一个伪 JSX 组件(vdom 对象):
const fakeComp = {
type: 'h1',
props: {
children: [
'Hello, Slim-React!'
]
}
}
而 render 方法需要把这个 vdom 最终渲染在页面指定的容器中。简单粗暴,完善一下上面的 createRoot :
function createRoot(container) {
return {
render(App) {
const appEl = document.createElement(App.type)
appEl.innerText = App.props.children
container.append(appEl)
}
}
}
如此这般,我们就可以如 React 文档所描述那样运行起来了:
const fakeComp = {
type: 'h1',
props: {
children: 'Hello, Slim-React!',
},
};
// Clear the existing HTML content
document.body.innerHTML = '<div id="app"></div>';
// Render your React component instead
const root = createRoot(document.getElementById('app'));
root.render(fakeComp);
当然,我们真正想要的是渲染 JSX 组件。但是在此之前,我们想要更深层地弄清楚 JSX 组件最终被转化成的 vdom 对象的结构。所以修改一下前面的 React Playground 代码,看看一个稍微复杂的组件是什么样的:

可以简单总结为以下规律:
type描述节点标签类型props存储当前节点的属性值以及特殊字段childrenchildren字段存储当前节点的子节点,如果当前节点内部只有一个文本节点,则为字符串类型;否则为数组类型,存储多个节点。
依照以上规律,我们可以调整 render 的实现:
export function createRoot(container) {
return {
render(App) {
updateChildren(App, container);
},
};
}
function updateChildren(vdom, container) {
const el = document.createElement(vdom.type);
Object.keys(vdom.props).forEach((key) => {
if (key === 'children') {
if (typeof vdom.props[key] === 'string') {
el.innerText = vdom.props[key];
} else {
vdom.props[key].forEach((child) => updateChildren(child, el));
}
} else {
el[key] = vdom.props[key];
}
});
container.append(el);
}
FakeComp 调整为:
const fakeComp = {
type: 'div',
props: {
children: [
{
type: 'h1',
props: {
id: 'title',
children: 'Hello, Slim-React!',
},
},
{
type: 'p',
props: {
children: 'oh I see!',
},
},
],
},
};
这时候我们就能渲染一个比较复杂的 vdom 了。
简单JSX 支持
既然我们现在已经可以渲染一个稍微复杂的,我们就可以考虑对的 JSX 支持了。
对于 JSX 的解析,如果你是大佬你当然可以自己手撸一个解析器;考虑到我是一个菜鸡,而恰好 Vite 对于 JSX 的支持是开箱即用的,遂跟着 Vite 的文档 快速新建一个基于 vanilla-js 的工程,再把以上代码迁移到项目的中。
然后就可以把 fakeComp 组件改成一个 JSX 组件,并把入口文件拓展名改成 jsx,此时项目目录结构如下:

此时运行项目,你会发现页面渲染异常了,控制台输出一个奇怪的错误:
Uncaught ReferenceError: React is not defined
WTF?我这明明是超瘦的 slim-react 啊,为何会对 React 有依赖?
这是对于拓展名以 .jsx / .tsx 结尾的文件其内部的 JSX 语法,Vite 会默认使用前面提到的 React.createElement 转化为 vdom,而此时我们的项目中并没有 React 这个依赖,所以会报错。所以,我们可以在 src 目录下增加一个 core.js,把原来 slim-react.js 的代码迁移到 core.js,然后吧 slim-react.js 作为统一的导出出口;并再增加一个 createElement 方法:
// core.js
export function createRoot ....
export function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.length > 1 ? children : children[0],
},
};
}
function updateChildren ...
// slim-react.js
import { createRoot, createElement } from './core';
const React = {
createRoot,
createElement,
};
export default React;
这样,我们的渲染就成功了!以下是本文的最终示例代码:slim-react-起步篇示例代码
到这里,起步篇就结束了。这个阶段,我们先是实现其基本的两个 API : createRoot 和 createElement,并通过分析 JSX 在渲染时的实际结构进行了简单的组件渲染逻辑。而下一步,我们会进一步探究 React 中的函数组件(Function Component)及 React Fiber —— 一个贯穿 React 的核心概念。
那我们,下次再会。
- To be continued -