手摸手实现超简易版mini-react(1)

981 阅读5分钟

本系列内容包含

  • 使用vite创建项目(解析jsx
  • 实现jsx挂载节点,运行起来项目
  • 实现vdom
  • 使用requestIdleCallback实现闲时加载
  • dom更新(增删改)
  • 支持FunctionComponent
  • 实现useState
  • 实现useEffect

本文为第1章节,实现前4条


创建项目

1. 使用vite创建项目:

npm create vite@latest

image.png 然后将这个项目跑起来:

image.png

2.实现jsx挂载节点,运行起来项目

众所周知, react项目使用的是jsx语法,我们使用vite构建的目的就是为了解析jsx文件(当然其他工具也可以)

而react项目初始化时main.jsx中的操作大致是:

image.png

我们先来实现前半部分,也就是:

ReactDom.createRoot(document.getElementById('app'))

在根目录中创建core目录,然后创建ReactDom文件:

image.png

创建createRoot方法,然后export

image.png

梳理一下目前可以想到的createRoot功能: 1.接受一个dom, 作为挂载的根节点 2.返回一个obejct, obejct中含有render方法

我们先忽略其他,实现给根节点挂载一个元素:

image.png 然后ReactDom.js中打印接收到的值:

image.png

切回浏览器,查看项目运行情况

发现报了一个React is not defind

image.png

这是因为vite解析jsx时默认使用react解析,

既然没有React,那我们就在main.jsx中创建一个React对象,看他怎么说:

image.png

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

image.png

这次不是Reactundefind了,而是React.createElement is not a function

那我们就给React对象加一个createElemnt方法,并打印接收到的参数:

image.png

然后控制台查看打印:

image.png

可以看到,这是我们render函数中传入dom的描述,其中第一个参数是元素类型,第二个参数是元素的props,第三个参数是子节点,再结合他的名字createElement,我们不难猜出,这个方法是根据jsxdom的描述创建一个dom对象

既然如此,我们就实现这个简单功能:

image.png

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

image.png

返回页面,渲染也正常:

image.png

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

我们修改一下dom结构:

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

这样浏览器就渲染正常:

image.png

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

image.png

这样我们就实现了 ReactDom.createRoot(document.getElementById('app'))

3.实现vdom

但是还有一个问题:react使用的是vdom(Virtual DOM),根据vdom合适的时间进行渲染,我们现在要考虑如何实现它:

首先是vdomvdom就是一个可以描述dom节点的object对象,react使用链表的方式表示各个节点的关系。

我们先将React.createElement创建dom改为创建vdom:

image.png

需要注意的是children字符串时,创建对应的TextNode。

那么如何将vdom转化为链表呢?

这时ReactDom.redner会报错:

image.png

这是因为他接收的dom是我们的vdom描述了。

我们在ReactDom.render中进行递归生成dom并挂载:

image.png

这样页面渲染正常:

image.png

4.实现闲时加载 requestIdleCallback API

使用[requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)

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

先简单使用一下这个api:

image.png

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

image.png

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

image.png

image.png

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

我们当前的vdom结构为:

image.png

转化为:

root -> foot -> text -> bar -> text

我们把方法命名为perWorkOfUnit, perWorkOfUnit先实现render方法的创建dom,赋值props:

image.png

给元素转换链表:

image.png

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

image.png

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

image.png

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

image.png

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

image.png

哪里出了问题?

通过debugger可以看到,函数在foo下的text执行完后就停止了,显示fiberchildsibling都为空。

再看图:

image.png

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

image.png

这样页面就渲染正常了:

image.png

这样我们的闲时加载就完成了!

仓库地址:github.com/mmmmr/mini-… 这个是全的


下节预告:实现FunctionComponent与修改(新增删除修改)


@崔佬的mini-react 游戏副本