框架原理 —— mini-react
00 分钟
2023-3-20
2023-3-20
type
status
date
slug
summary
tags
category
icon
password
Edited
Mar 20, 2023 11:37 AM
Created
Mar 20, 2023 04:58 AM

MReact (Mock React)

MReact目标是, 通过提供相同API的更简单实现, 以及如何构建它的逐步说明,使React的内部更容易理解. 一旦你理解了React如何在内部工作,使用它将更容易.
模板:
ps:环境不是很好实现

JSX

JSX 实际上是一种语法糖,它本质上其实是 React.createElement(type, props, ...children) 的简易方式。
例如:
实际上是:

Babel

为了使浏览器的理解,需要的代码由预处理转化为-有效的JS,像babel
babel从上面将JSX转换为:
你也可以在线体验一些 Babel 的转换

对于 { 变量 }

Babel会对该变量自动解析成一个新的节点
例如:
转换:

自定义配置React的createElement

/** @jsx MReact.createElement */ 自定义自己实现的createElement方法
 

Step I: The createElement Function

createTextElement

Babel转换

根据我们自定义的createElement方法,转换为

Step II: The render Function

当前结构代码

index.js

mreact.js

Step III: Concurrent Mode

并发模式

问题

让我们注意一下这段代码(这个递归调用有问题
一旦我们开始渲染,直到渲染完完整的元素树才会停止。如果元素树很大,它可能会阻塞主线程太久。如果浏览器需要做高优先级的事情,比如处理用户输入或保持动画流畅,它必须等到渲染完成。
比如处理用户输入或保持动画流畅,它将不得不等待渲染完成。
所以我们要把工作分解成小的单元,当我们完成每个单元后,如果还有其他需要做的事情,我们会让浏览器中断渲染。
接下来先认识一个Web API

requestIdleCallback

requestIdleCallback 会在帧结束时并且有空闲时间。或者用户不与网页交互时,执行回调
  • 回调接受一个参数 deadlinedeadline是一个对象,对象上有两个属性
  • timeRemainingtimeRemaining属性是一个函数,函数的返回值表示当前空闲时间还剩下多少时间
  • didTimeoutdidTimeout属性是一个布尔值,如果didTimeout是true

单元拆分执行实现

Step IV: Fibers

为了组织工作单元,我们需要一个数据结构:fiber树
我们将为每个元素提供一个fiber,每个fiber将成为一个unit
例子:
fiber树结构:
notion image

所需要做的

在渲染中,我们将创建root fiber并将其设置为 nextUnitOfWork。剩下的工作将发生在 performUnitOfWork函数上,我们将为每个fiber做三件事:
  • 将元素添加到 DOM
  • 为元素的子元素创建fiber
  • 选择下一个unit
当我们完成对fiber的工作时,如果它有一个子fiber,那么该纤程将成为下一个unit
如果fiber没有孩子,我们使用sibling作为下一个unit
如果fiber既没有孩子也没有sibling,我们就去找“叔叔”:父母的兄弟姐妹。
此外,如果parent没有sibling,我们会继续向上遍历parent,直到我们找到一个有siblingparent或直到我们到达root。如果我们已经到达根目录,则意味着我们已经完成了此渲染的所有工作。

实现

creatDOM

将之前render函数创建dom的行为分离出来

render

render现在标记下一次需要执行的工作

performUnitOfWork

performUnitOfWork现在用于每一个元素的创建和创建fiber树的主要逻辑,并将fiber树拆分成一个个叶子节点为一个个unit工作单元来执行

Step V: Render and Commit Phases

问题

每次处理一个元素时,我们都会向 DOM 添加一个新节点。请记住,浏览器可能会在我们完成整个树的渲染之前中断我们的工作。在这种情况下,用户会看到一个不完整的用户界面。我们不希望那样。
所以将节点绑定到页面的操作要换个地方执行

render

记录fiber树

commit

当fiber树构建完成后,在commitRoot中将所有的fiber添加到页面
即将整个fiber树提交给DOM

workLoop

Step VI: Reconciliation

问题

到目前为止,我们只向 DOM 添加内容,但是更新或删除节点呢?
这就是我们现在要做的,我们需要将我们在渲染函数上接收到的元素与我们提交给DOM的最后一个 fiber树进行比较。
因此,我们需要在完成提交后保存对“我们提交给 DOM 的最后一个fiber树”的引用。我们称之为 currentRoot
我们还为每个fiber添加了alternate属性。此属性是指向旧fiber的链接,旧fiber是我们在前一个提交阶段提交给 DOM 的fiber

添加

reconcileChildren

现在让我们从创建新纤程的 performUnitOfWork 中提取代码到一个新的 reconcileChildren 函数。
 
比较旧fiber和新元素,我们使用类型:
  • 如果旧fiber和新的元素有相同的类型,我们可以保留 DOM 节点并用新的 props 更新它
  • 如果类型不同并且有一个新元素,则意味着我们需要创建一个新的 DOM 节点
  • 如果类型不同并且有旧fiber,我们需要删除旧节点
 
还需要添加一个deletions数组,用于记录要删除的fiber
 

commit

updateDom

更改creatDom

 

当前结构代码

到目前为止,整体架构差不多完成,剩下的都是一些零碎的

index.js

mreact.js

Step VII: Function Components

问题

函数组件在两个方面有所不同:
  • 来自函数组件的fiber没有DOM节点
  • children来自运行函数而不是直接从props中获取
 

updateHostComponent

performUnitOfWork中普通元素的逻辑提取出来处理

performUnitOfWork

updateFunctionComponent

commit

修改domParent的逻辑
同理,修改删除的逻辑

commitDeletion

Step VIII: Hooks

典型例子:

前提条件

我们需要在调用函数组件之前初始化一些全局变量,以便我们可以在 useState 函数内部使用它们。
首先我们设置正在进行的wipFiber
我们还在 fiber 中添加了一个 hooks 数组,以支持在同一个组件中多次调用 useState。我们会跟踪当前的挂钩索引。

useState

 
就这样。我们构建了自己的 React 版本。

完整代码

index.js

mreact.js

 

参考链接

  1. Build your own React (pomb.us)
 
上一篇
tRPC & Zod
下一篇
框架原理 —— mini-vue

评论
Loading...