用400行代码构建你自己的React.js

原文信息: 查看原文查看原文

Build Your Own React.js in 400 Lines of Code

- Zachary Lee

React v19 beta已经发布。与React 18相比,它提供了许多用户友好的API,尽管其核心原理大致保持不变。你可能已经使用React有一段时间了,但你知道它在底层是如何工作的吗?

本文将帮助你用大约400行代码构建一个支持异步更新和可中断的React版本——这是React的核心特性,许多高级API都依赖于此。下面是最终效果的Gif:

我使用了React官方网站提供的井字棋教程示例,可以看到它运行良好。

它目前托管在我的GitHub上,你也可以访问在线版本亲自试试。

JSX 和 createElement

在深入探讨mini-react.ts的原理之前,理解JSX代表什么是很重要的。我们可以使用JSX来描述DOM并轻松应用JavaScript逻辑。然而,浏览器不原生支持JSX,所以我们编写的JSX需要被编译成浏览器可以理解的JavaScript。

这里我使用了Babel,但你当然可以使用其他构建工具,它们生成的内容将会相似。

你可以看到它调用了React.createElement,提供了以下选项:

  1. type: 表示当前节点的类型,例如div

  2. config: 代表当前元素节点的属性,例如{id: "test"}

  3. children: 子元素,可以是多个元素、简单文本或更多由React.createElement创建的节点。

如果你是一个经验丰富的React用户,你可能还记得在React 18之前,为了正确编写JSX,你需要import React from 'react';。自React 18以来,这不再是必要的,提升了开发者体验,但底层仍然会调用React.createElement

对于我们简化的React实现,我们需要配置Vitereact({ jsxRuntime: 'classic' }),将JSX直接编译为React.createElement实现。

然后我们可以实现我们自己的

# Render

接下来,我们基于前面创建的数据结构实现一个简化版本的渲染函数,将JSX渲染到真实的DOM中。

这是在线实现链接。它目前仅渲染一次JSX,因此不处理状态更新。

Fiber架构和并发模式

Fiber架构和并发模式主要是为了解决一旦递归处理完整个元素树,无法中断的问题,可能会长时间阻塞主线程。高优先级任务(如用户输入或动画)可能无法及时处理。

在其源代码中,工作被分解为小单元。每当浏览器空闲时,它处理这些小的工作单元,将控制权交还给主线程,使浏览器能够及时响应高优先级任务。一旦所有小单元的工作完成,结果映射到真实DOM。

React Conf 2024

在真实的React中,我们可以使用其提供的API,如useTransitionuseDeferredValue,来显式降低更新的优先级。

总结一下,这里的两个关键点是如何释放主线程以及如何将工作分解为可管理的单元。

requestIdleCallback

requestIdleCallback是一个实验性API,当浏览器空闲时执行回调。它还未被所有浏览器支持。在React中,它被用于scheduler package,其调度逻辑比requestIdleCallback更复杂,包括更新任务优先级。

但在这里我们只考虑异步可中断性,所以这是模仿React的基本实现:

这里是一些关键点的简要说明:

为什么使用MessageChannel?

主要是因为它使用宏任务来处理每一轮的单元任务。但为什么是宏任务?

这是因为我们需要使用宏任务来释放主

线程控制,使浏览器能够在这段空闲时间内更新DOM或接收事件。因为浏览器将DOM更新作为一个独立的任务,而此时JavaScript不会执行。

主线程一次只能运行一个任务——要么执行JavaScript,要么处理DOM计算、样式计算、输入事件等。而微任务则不会释放主线程控制。

为什么不使用setTimeout?

这是因为现代浏览器认为嵌套的setTimeout调用超过五次会导致阻塞,并将其最小延迟设置为4毫秒,所以不够精确。

算法

请注意,React不断演变,我描述的算法可能不是最新的,但它们足以理解其基本原理。

这是一个显示工作单元之间连接的图:

在React中,每个工作单元称为一个Fiber节点。它们使用类似链表的结构链接在一起:

  1. child: 从父节点指向第一个子元素的指针。

  2. return/parent: 所有子元素都有一个指针指回父元素。

  3. sibling: 从第一个子元素指向下一个兄弟元素的指针。

有了这个数据结构,让我们看看具体实现

我们只是扩展了render逻辑,重组了调用顺序为workLoop -> performUnitOfWork -> reconcileChildren -> commitRoot

  1. workLoop : 通过连续调用requestIdleCallback获取空闲时间。如果当前空闲并且有单元任务要执行,则执行每个单元任务。

  2. performUnitOfWork: 具体执行的单元任务。这是链表思想的体现。具体来说,每次只处理一个fiber节点,并返回要处理的下一个节点。

  3. reconcileChildren: 调和当前fiber节点,实际上是虚拟DOM的比较,并记录需要进行的更改。你可以看到我们直接在每个fiber节点上修改和保存,因为现在这只是对JavaScript对象的修改,不涉及真实DOM。

  4. commitRoot: 如果当前需要更新(根据wipRoot)并且没有下一个单元任务要处理(根据!nextUnitOfWork),这意味着虚拟更改需要映射到真实DOM。commitRoot就是根据fiber节点的更改修改真实DOM。

有了这些,我们可以真正使用fiber架构进行可中断的DOM更新,但我们仍然缺少一个触发器。

触发更新

在React中,常见的触发器是useState,这是最基本的更新机制。让我们实现它来点燃我们的Fiber引擎。

这是具体实现:

它巧妙地将钩子的状态保存在fiber节点上,并通过队列修改状态。从这里你也可以看到为什么React钩子的调用顺序不能改变。

结论

我们实现了一个支持异步和可中断更新的React最小模型,无依赖,排除注释和类型,可能少于400行代码。希望这对你有所帮助。

分享于 2024-05-17

访问量 32

预览图片