本系列内容包含:
- 使用
vite
创建项目(解析jsx
) - 实现
jsx
挂载节点,运行起来项目 - 实现
vdom
- 使用
requestIdleCallback
实现闲时加载 -
dom
更新(增删改) - 支持
FunctionComponent
- 实现
useState
- 实现
useEffect
本文为第1章节,实现前4条
创建项目
1. 使用vite创建项目:
npm create vite@latest
然后将这个项目跑起来:

2.实现jsx挂载节点,运行起来项目
众所周知, react项目使用的是jsx语法,我们使用vite构建的目的就是为了解析jsx文件(当然其他工具也可以)
而react项目初始化时main.jsx中的操作大致是:

我们先来实现前半部分,也就是:
ReactDom.createRoot(document.getElementById('app'))
在根目录中创建core目录,然后创建ReactDom文件:

创建createRoot方法,然后export

梳理一下目前可以想到的createRoot功能: 1.接受一个dom, 作为挂载的根节点 2.返回一个obejct, obejct中含有render方法
我们先忽略其他,实现给根节点挂载一个元素:
然后ReactDom.js中打印接收到的值:

切回浏览器,查看项目运行情况
发现报了一个React is not defind

这是因为vite
解析jsx
时默认使用react
解析,
既然没有React,那我们就在main.jsx中创建一个React对象,看他怎么说:

发现又报了一个新的错误:

这次不是React
是undefind
了,而是React.createElement is not a function
那我们就给Reac
t对象加一个createElemnt
方法,并打印接收到的参数:

然后控制台查看打印:

可以看到,这是我们render
函数中传入dom
的描述,其中第一个参数是元素类型,第二个参数是元素的props,第三个参数是子节点,再结合他的名字createElement
,我们不难猜出,这个方法是根据jsx
中dom
的描述创建一个dom
对象
既然如此,我们就实现这个简单功能:

处理完毕,我们返回控制台发现这次并没有报错,而且ReactDom
中的render
函数打印到了值,这下我们终于可以将dom挂载到根节点了!

返回页面,渲染也正常:

但这时候就有问题了,我们总不能只挂载一个元素吧?如果挂载很多我们还能支持吗?
我们修改一下dom结构:

回到浏览器,发现渲染了一些奇怪的东西:

那看来是React.createElemnt
创建dom有问题,回到React.createElement
打印:

发现函数被执行了3次,每次分别打印为:

我们发现接收到的children
不对呀,明明div id=app
里有两个子元素,怎么只收到一个呢?这是因为createElement
参数中children
并不是以数组的方式传入,而是从第三个参数开始每个子元素传入一个参数,所以我们可以这样接收:

现在我们再打印,接受到的children就正常了:

那我们我们再更改一下我们创建子节点的方法,让他支持多级:

这样浏览器就渲染正常:

我们可以把React提取到/core/React.js文件中

这样我们就实现了 ReactDom.createRoot(document.getElementById('app'))
3.实现vdom
但是还有一个问题:react使用的是vdom(Virtual DOM)
,根据vdom
在 合适的时间
进行渲染,我们现在要考虑如何实现它:
首先是vdom
,vdom
就是一个可以描述dom
节点的object
对象,react
使用链表
的方式表示各个节点的关系。
我们先将React.createElement
由创建dom
改为创建vdom
:

需要注意的是children
为字符串
时,创建对应的TextNode。
那么如何将vdom
转化为链表
呢?
这时ReactDom.redner
会报错:

这是因为他接收的dom
是我们的vdom
描述了。
我们在ReactDom.render
中进行递归生成dom
并挂载:

这样页面渲染正常:

4.实现闲时加载 requestIdleCallback API
使用[requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)
简单放个描述:window.requestIdleCallback()
方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout
,则有可能为了在超时前执行函数而打乱执行顺序
先简单使用一下这个api
:

这样控制台就会在空闲时打印count

我们将ReactDom.Render
放到React.js
中,ReactDom
引入React
并使用React.render
,这样我们的逻辑都放到React.js
文件中


之后我们将vdom
转换为链表
结构,使用child
字段表示第一个子元素,使用sibling
表示第一个兄弟元素, 使用parent
元素表示父元素。这样方便我们使用requestIdleCallback
每次只调用一个元素。
我们当前的vdom
结构为:

转化为:
root -> foot -> text -> bar -> text
我们把方法命名为perWorkOfUnit
, perWorkOfUnit
先实现render
方法的创建dom
,赋值props
:

给元素转换链表:

这样我们就可以生命一个全局变量nextUnitOfWork
,在render
中将要处理的元素赋值到nextUnitOfWork
, 然后使用requestIdleCallback
进行闲时处理:

回到页面,发现最外层的app
节点已经挂载了,而他的子元素并没有挂载:

这是因为我们render
中只传入了app
节点,我们需要在每次perWorkOfUnit
执行了后需要将nextUnitOfWork
赋值 给当前节点的child
或者sibling
,这样才会往下进行:

再次回到页面,发现这次foo
节点渲染了,bar
节点没有渲染:

哪里出了问题?
通过debugger可以看到,函数在foo
下的text
执行完后就停止了,显示fiber
的 child
与sibling
都为空。
再看图:

不难发现,我们foo/text
的sibling
确实是空的,这时我们应该执行的是foo
的sibling
也就是text
的parent
节点的sibling
,我们可以通过一个while
循环处理:

这样页面就渲染正常了:

这样我们的闲时加载就完成了!
仓库地址:github.com/mmmmr/mini-... 这个是全的
下节预告:实现FunctionComponent与修改(新增删除修改)
@崔佬的mini-react 游戏副本