‘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
存储当前节点的属性值以及特殊字段children
children
字段存储当前节点的子节点,如果当前节点内部只有一个文本节点,则为字符串类型;否则为数组类型,存储多个节点。
依照以上规律,我们可以调整 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 -