【CSDN 编者按】400行代码在React 18中实现可中断的异步更新的最小模型!
声明:本文为 CSDN 翻译,转载请注明来源。
以下为译文:
React v18已经发布,它给我们带来了许多特性,但最重要的特性是可中断的异步更新,许多新的上层API都是通过它创建的。可以说,它是React v18的底层引擎。
本文将使用大约400行代码带你实现一个可以异步更新和可中断更新的Mini-React,如下图所示:
我使用了React官方网站提供的tic-tac-toe教程示例(以下是链接:https://reactjs.org/tutorial/tutorial.html#what-are-we-building),可以看到它非常有效。此外,它目前支持函数组件和类组件,可以满足开发者80%的需求!我也把它放在GitHub上(以下是链接:https://github.com/islizeqiang/mini-react),你也可以在本地复制它,并按照我的文章一步一步地调试。
这是我在阅读了大量React的源代码后创建的,在整体逻辑和函数命名上基本上和React一样,如果你对React的内部原理感兴趣,这篇文章就是为你准备的!
JSX和createEelement
我相信你对 React 中的 JSX 并不陌生。我们使用 JSX 来描述 DOM,它们最终会被 babel 转换成 React 提供的 API。例如下面的代码:
你也可以自己在StackBlitz上试试(在终端输入node transform-JSX.js):
// run `node transform-JSX.js` in the terminal
const babel = require('@babel/core');
const optionsObject = {
presets: ['@babel/preset-env'],
plugins: [['@babel/plugin-transform-react-jsx']],
};
const { code } = babel.transformSync(
'const element =
',Hello
optionsObject
);
console.log(code);
你还可以在编译好的字符串中加入更多的元素,再看看最终的结果,我在这里直接给出React.createElement提供的选项。
1.type:表示当前节点的类型,如上图中的div。
2.config:表示当前元素节点上的属性,如上图中的{id: "test"}。
3.children:子元素,可以是多个、简单的文本,也可以由React.createElement创建的子节点。
然后根据这个要求实现你自己的React.createElement,就像下面的代码一样,我们定义一个自定义的数据结构。
然后我们可以根据上面创建的数据结构实现一个简化版的渲染函数,将JSX渲染到真实的DOM上。
下面的代码演示将使用CodeSandbox,拖动左栏查看代码,点击上方的菜单按钮查看目录结构。你也可以直接编辑,查看显示的结果。
import React from "./mini-react";
const App = (
<div id="test">
<h1>Hello</h1>
</div>
);
// eslint-disable-next-line react/no-deprecated
React.render(App, document.getElementById("root"));
所以你可以看到它工作,但现在它只渲染一次,不能与我们互动。
另外,请注意我们在这里使用react-scripts@3.4.4来帮助编译JSX,API在以后的版本中已经发生了变化,但是React.createElement在结束时仍然被调用。我提供的GitHub库使用了Vite而不是react-scripts。
接下来,是React的核心纤程架构和并发模式,这是从React 17开始提出的,主要是为了解决一旦完整的元素树被递归,就无法终止的问题,这可能导致主线程长时间被阻塞,那些高优先级的任务(比如那些用户输入或动画等)无法及时处理。
所以在React源代码中,工作被分解成小单元。一旦浏览器处于空闲状态,它将处理这些小的工作单元,然后将结果映射到实际的DOM,直到所有结果都被处理完。
requestIdleCallback是一个实验性API,它在浏览器空闲时执行回调。接下来,我们将使用这个API来简单地实现这个功能。我将在最后给出React目前使用的调度程序包的模拟实现。
在开始编写下一个代码之前,我想再次介绍一下工作单元之间的连接。
就像上面的图片一样,我们将像链表一样创建每个纤程节点之间的连接,它们是:
1.child:父节点指向第一个子元素的指针。
2.return/parent:所有子元素都有一个指向父元素的指针。
3.sibling:从第一个子元素指向下一个同级元素。
所以现在你可以愉快地编写代码:
import React from "./mini-react";
const App = (
<div id="test">
<h1>Hello</h1>
</div>
);
// eslint-disable-next-line react/no-deprecated
React.render(App, document.getElementById("root"));
尽管添加了这么多代码,我们只是重构了渲染逻辑。重构后的调用顺序为workLoop →performUnitOfWork→reconcileChildren。下面让我来总结一下各个功能的作用:
1.workLoop:通过连续调用requestIdleCallback来获得空闲时间。如果当前空闲且有单元任务要执行,则执行每个单元任务。
2.performUnitOfWork:执行的特定单元任务。这是链表思想的体现。即一次只处理一个纤程节点,并返回下一个要处理的节点。
3.reconcileChildren:协调当前纤程节点,它实际上是虚拟DOM的比较,并记录要进行的更改。你可以看到,我们直接在每个纤程节点上修改和保存,因为现在它只是对JavaScript对象的修改,而不涉及真正的DOM。
4.最后一步是commitRoot。如果当前需要更新(根据wipRoot),并且没有下一个单元任务要处理(根据!nextUnitOfWork),这意味着需要将虚拟更改映射到实际的DOM。commitRoot负责根据纤程节点的变化修改真实的DOM。
到目前为止,我们已经实现了纤程架构,是时候见证它的威力了。
我们想给组件添加状态,让我们实现一个useState。
import React from "./mini-react";
import "./styles.css";
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i += 1) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return ;
}
class Square extends React.Component {
render() {
return (
<button onClick={this.props.onClick} className="square">
{this.props.value}
);
}
}
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => {
this.props.onClick(i);
}}
/>
);
}
render() {
return (
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}