百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

React 18发布,仅用400行代码就能实现一个Mini-React

liuian 2025-03-07 20:53 44 浏览

【CSDN 编者按】400行代码在React 18中实现可中断的异步更新的最小模型!

原文链接:https://betterprogramming.pub/react-18-has-been-released-implement-mini-react-in-400-lines-of-code-837559761758

:本文为 CSDN 翻译,转载请注明来源。

作者 | Zachary Lee 译者 | 彭慧中
责编 | 屠敏
出品 | CSDN(ID:CSDNnews)

以下为译文:

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-deprecatedReact.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-deprecatedReact.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)}
); }}
class App extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill() } ], stepNumber: 0, xIsNext: true }; }
handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; }
squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); }
jumpTo(step) { this.setState({ stepNumber: step, xIsNext: step % 2 === 0 }); }
render() { const { history } = this.state; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { const desc = move ? `Go to move #${move}` : "Go to game start"; return (
  • ); });
    let status; if (winner) { status = `Winner: ${winner}`; } else { status = `Next player: ${this.state.xIsNext ? "X" : "O"}`; }
    return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i) => { this.handleClick(i); }} />
    <div className="game-info">
    {status}
      {moves}
    ); }}
    // eslint-disable-next-line react/no-deprecatedReact.render(, document.getElementById("root"));


    useState巧妙地将hook的状态保留在纤程节点上,并通过队列修改状态。从这里,我们也可以知道为什么React-hooks要求每次调用的顺序不能改变。

    除此以外,我们还实现了一个Component ,这里只是简单地转换为一个渲染的方法,并添加了一点它的独特身份。


    模拟requestIdleCallback


    现在我们几乎实现了所有的功能,让我解释一下React目前采用的调度器包,它实际上是一个比requestIdleCallback更复杂的调度逻辑,包括更新任务的优先级等等。

    上面是我实现模拟requestIdleCallback的参考调度程序,它结合了requestAnimationFrame和MessageChannel。这里使用MessageChannel的目的是使用宏任务来处理每一轮的单元任务。

    那么为什么要使用宏任务呢?

    为了放弃主线程,浏览器可以在空闲期间更新DOM或接收事件。因为浏览器更新DOM是一个独立的任务,而JavaScript在这个时候不会被执行,因为主线程一次只能运行一个功能,要么执行JS,要么处理DOM计算样式,要么接收输入事件,等等。

    为什么不使用微任务呢?

    因为微任务包含在每一轮宏任务中,所以在所有微任务执行完毕之前,也就是当前宏任务未完成时,主线程不能放弃。

    为什么不使用setTimeout呢?

    因为如果setTimeout被嵌套调用超过5次,该函数将被视为阻塞,浏览器将把最小时间设置为4ms,所以它不够精确。

    最终版本

    下面是最终的版本,你可以看到,在去掉注释后,不到400行代码就实现了React的核心思想。

    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)}
    ); }}
    class App extends React.Component { constructor(props) { super(props); this.state = { history: [ { squares: Array(9).fill() } ], stepNumber: 0, xIsNext: true }; }
    handleClick(i) { const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; }
    squares[i] = this.state.xIsNext ? "X" : "O"; this.setState({ history: history.concat([ { squares } ]), stepNumber: history.length, xIsNext: !this.state.xIsNext }); }
    jumpTo(step) { this.setState({ stepNumber: step, xIsNext: step % 2 === 0 }); }
    render() { const { history } = this.state; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
    const moves = history.map((step, move) => { const desc = move ? `Go to move #${move}` : "Go to game start"; return (
  • ); });
    let status; if (winner) { status = `Winner: ${winner}`; } else { status = `Next player: ${this.state.xIsNext ? "X" : "O"}`; }
    return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={(i) => { this.handleClick(i); }} /> <div className="game-info">
    {status}
      {moves}
    ); }}
    // eslint-disable-next-line react/no-deprecatedReact.render(, document.getElementById("root"));

    我还在GitHub中添加了一个TypeScript版本的Mini-React
    https://github.com/islizeqiang/mini-react/blob/master/src/mini-react.ts)
    ,如果你有兴趣,可以去看看。

    END


    成就一亿技术人

    相关推荐

    安卓软件apk安装包下载(安卓apk安装器下载)
    安卓软件apk安装包下载(安卓apk安装器下载)

    1.设定-安全-未知来源-打钩。2.可能是下载的软件安装包不完整,请重新下载安装。3.可能是安装软件的版本低于现有版本、或者软件兼容性问题所导致,请更换其他版本或者其他软件再次下载安装。4.安卓手机支持的软件格式为APK,这种情况可以更换其...

    2026-01-07 12:05 liuian

    一千左右的笔记本电脑哪个好

    ¥1000左右,笔记本性价比最高的也就性价比之王的肯定,是联想的这种脾气吧,因为联想的笔记本的是国产的最好的这种脾气吧,他生产的笔记本那有高档中档和低端的这几种所以¥1000左右的可以买一杯联想比较性...

    电脑截屏的快捷按键(“电脑截屏的快捷键”)

    电脑截图快捷键是Ctrl+PrScrn。电脑上截图的方法一般有这4种方式:1、Ctrl+PrScrn使用这个组合键截屏,获得的是整个屏幕的图片;2、Alt+PrScrn这个组合键截屏,获得的结果是当...

    qq空间为什么打不开了怎么办
    qq空间为什么打不开了怎么办

    1、首先我们需要在自己的手机上下载并安装最新版本的QQ空间,安装完成后打开QQ空间,登录自己的账号。2、首先我们尝试打开QQ空间,打不开在手机上找到设置,点击进入设置界面,在这里我们可以对手机进行部分设置。3、进入设置界面后,我们需要在其中...

    2026-01-07 10:55 liuian

    联想怎么恢复系统(联想电脑系统恢复怎么操作)

      如果电脑重新分区,安装了操作系统就无法恢复到预装系统。  安装了系统,没有做系统备份也是无法恢复的,只能够重新安装。  联想电脑的一键还原系统在需要使用的时候直接按下一键还原按键即可,系统会自动进...

    台式机无线网卡连不上wifi怎么办

    1、无线网卡驱动没更新或者不匹配;解决办法:开始→控制面板→系统→硬件→设备管理器→网络适配器→右击无线网卡→更新驱动程序软件→按部就班完成更新;2、无线网络服务未开通;解决办法:开始→控制面板→管理...

    126官方邮箱登录入囗(126邮箱登录入口手机版官网)

    126邮箱是网易的一个免费邮箱登录入口在浏览器输入:mail.126.com浏览器进入126邮箱网站之后,即可自行登录126邮箱账号。如何在手机上登录126邮箱1、打开手机,找到并打开网易邮箱软件。2...

    耐用笔记本电脑排行(最耐用笔记本排名)

    最耐用的笔记本电脑是联想牌子。联想绝对是最耐用的,质量最好的轻薄本,也是最好用的轻薄本之一。联想笔记本电脑的质量和做工在世界上能称第一。十大公认最耐用笔记本如下1、华硕灵耀x14第12代英特尔酷睿i...

    固态硬盘量产工具下载(固态硬盘量产工具下载合集)

    固态硬盘量产工具好用的依次是:SLC颗粒最好,MLC颗粒其次,TLC颗粒再次,QLC颗粒现在最次;但是颗粒也分原厂颗粒,黑片,白片;原厂最好,黑片就是淘汰下来的渣渣。slc最好寿命长速度快也最贵...

    文件夹怎么显示隐藏的文件夹

    查找隐藏文件夹的方式如下:1、点击桌面的“计算机”,双击打开计算机2、在计算机页面中,点开自己存储文件夹的盘,这里以c盘为例。其他盘也可以依据这样的方式进行查找。3、找到左上角的“组织”,在其下拉的菜...

    wifi一键连(wifi一键连接下载)
    • wifi一键连(wifi一键连接下载)
    • wifi一键连(wifi一键连接下载)
    • wifi一键连(wifi一键连接下载)
    • wifi一键连(wifi一键连接下载)
    电脑硬盘怎么格式化干净(电脑硬盘格式化详细过程)
    电脑硬盘怎么格式化干净(电脑硬盘格式化详细过程)

    用普通格式化硬盘最干净,采用普通格式化,可以将硬盘中的内容删除掉,同时做上标志,而快速格式化只是对硬盘做了格式化标志,而没有删除内容硬盘格式化步骤:1,打开“我的电脑”,可以看到已经分区好但需要重新格式化的硬盘。2,用右键点击需要格式化的硬...

    2026-01-07 06:55 liuian

    下载的歌曲怎么转换成mp3格式

    一般酷狗音乐下载的都是MP3格式的啊,如果不是的话,也可以通过以下方法来转换。;1、打开浏览器搜索关键词,找到该软件后下载并安装,然后双击打开,再点击“添加文件”。;2、添加后,在右下角选择转换格式为...

    所有cpu性能天梯图(所有cpu性能天梯图都一样吗)

    1、英特尔(Intel)i59400F酷睿六核盒装CPU处理器;2、英特尔(Intel)i7-9700K酷睿八核盒装CPU处理器;3、英特尔(Intel)i78700酷睿六核盒装...

    电脑无故占用大量内存(电脑无故占用大量内存怎么办)

    一种可能是后台运行太多,可能是你最近进行了很多任务,导致电脑占用内存变大。也可能是内在任务,类似于你引入了潜在任务,即电脑程序负荷太重,且程序存在不稳定因素,任务自动执行以缓解电脑压力。这个一般没事的...