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

本系列内容包含:

  • 使用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

那我们就给React对象加一个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 游戏副本

相关推荐
liuchangng7 小时前
React 19 + Vite 6 实战:从教材 PDF 到 866 词间隔重复单词 App
前端·react.js·pdf·vite·开源项目
鸿蒙开发20 小时前
Three.js 首帧卡顿排查:compileAsync 之后,为什么还会停一下?
react.js·three.js
小李科技1 天前
【性能优化】事件委托:目标定位与 React/Vue 实战
前端·vue.js·react.js·面试·性能优化·前端框架
csj501 天前
前端基础之《React(16)—Modal组件封装》
前端·react.js
ZoeLandia3 天前
React11:数据请求与 TanStack Query
前端·react.js·typescript·react
瓦瓦gogo3 天前
实习中接触前端开发后,我开始了解 React
前端·react.js·前端框架
ZoeLandia4 天前
React08:自定义 Hook
开发语言·前端·react.js·typescript
空中湖6 天前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
来一首_We_go7 天前
React Flow Pro 高级交互实现
前端·react.js
weedsfly7 天前
React 不可变数据:为什么页面“明明数据变了却不更新
前端·javascript·react.js