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

本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com

【CSDN 编者按】400 行代码在 React 18 中实现可中断的异步更新的最小模型! 我相信你对 React 中的 JSX 并不陌生。

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

原文链接:https://betterpr

ogramming.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,如下图所示:

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/T4GTXksB63BPoR?from=pc

我使用了 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 的内部原理感兴趣,这篇文章就是为你准备的!

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/SibV8iB7gfIVeO?from=pc

JSX 和 createEelement

我相信你对 React 中的 JSX 并不陌生。我们使用 JSX 来描述 DOM,它们最终会被 babel 转换成 React 提供的 API。例如下面的代码:

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/T4GTXmG36nyN7L?from=pc

你也可以自己在 StackBlitz 上试试 (在终端输入 node transform-JSX.js):

1
// run `node transform-JSX.js` in the terminalconst babel = require('@babel/core');const optionsObject = { presets: ['@babel/preset-env'], plugins: [['@babel/plugin-transform-react-jsx']],};const { code } = babel.transformSync( 'const element = <div id="test"><h1>Hello</h1></div>', optionsObject);console.log(code);

你还可以在编译好的字符串中加入更多的元素,再看看最终的结果,我在这里直接给出 React.createElement 提供的选项。

1.type:表示当前节点的类型,如上图中的 div。

2.config:表示当前元素节点上的属性,如上图中的 {id: “test”}。

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

然后根据这个要求实现你自己的 React.createElement,就像下面的代码一样,我们定义一个自定义的数据结构。

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/SibV8qW8mqM9VB?from=pc

渲染

然后我们可以根据上面创建的数据结构实现一个简化版的渲染函数,将 JSX 渲染到真实的 DOM 上。

下面的代码演示将使用 CodeSandbox,拖动左栏查看代码,点击上方的菜单按钮查看目录结构。你也可以直接编辑,查看显示的结果。

1
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"));

所以你可以看到它工作,但现在它只渲染一次,不能与我们互动。

另外,请注意我们在这里使用 [email protected] 来帮助编译 JSX,API 在以后的版本中已经发生了变化,但是 React.createElement 在结束时仍然被调用。我提供的 GitHub 库使用了 Vite 而不是 react-scripts。

接下来,是 React 的核心纤程架构和并发模式,这是从 React 17 开始提出的,主要是为了解决一旦完整的元素树被递归,就无法终止的问题,这可能导致主线程长时间被阻塞,那些高优先级的任务(比如那些用户输入或动画等)无法及时处理。

所以在 React 源代码中,工作被分解成小单元。一旦浏览器处于空闲状态,它将处理这些小的工作单元,然后将结果映射到实际的 DOM,直到所有结果都被处理完。

requestIdleCallback 是一个实验性 API,它在浏览器空闲时执行回调。接下来,我们将使用这个 API 来简单地实现这个功能。我将在最后给出 React 目前使用的调度程序包的模拟实现。

在开始编写下一个代码之前,我想再次介绍一下工作单元之间的连接。

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/T4GTXmlFy951YA?from=pc

就像上面的图片一样,我们将像链表一样创建每个纤程节点之间的连接,它们是:

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

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

3.sibling:从第一个子元素指向下一个同级元素。

所以现在你可以愉快地编写代码:

1
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。

  1. 最后一步是 commitRoot。如果当前需要更新 (根据 wipRoot),并且没有下一个单元任务要处理 (根据! nextUnitOfWork),这意味着需要将虚拟更改映射到实际的 DOM。commitRoot 负责根据纤程节点的变化修改真实的 DOM。

到目前为止,我们已经实现了纤程架构,是时候见证它的威力了。

我们想给组件添加状态,让我们实现一个 useState。

1
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} class> {this.props.value} </button> ); }}class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={ => { this.props.onClick(i); }} /> ); } render { return ( <div> <div class> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div class> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div class> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); }}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 ( <li key={move}> <button onClick={ => this.jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = `Winner: ${winner}`; } else { status = `Next player: ${this.state.xIsNext ? "X" : "O"}`; } return ( <div class> <Board squares={current.squares} onClick={(i) => { this.handleClick(i); }} /> </div> <div class> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); }}// eslint-disable-next-line react/no-deprecatedReact.render(<App />, document.getElementById("root"));

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

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

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/SibV9897mQGxL0?from=pc

模拟 requestIdleCallback

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

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

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

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

为什么不使用微任务呢?

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

为什么不使用 setTimeout 呢?

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

最终版本

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

1
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} class> {this.props.value} </button> ); }}class Board extends React.Component { renderSquare(i) { return ( <Square value={this.props.squares[i]} onClick={ => { this.props.onClick(i); }} /> ); } render { return ( <div> <div class> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div class> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div class> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); }}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 ( <li key={move}> <button onClick={ => this.jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = `Winner: ${winner}`; } else { status = `Next player: ${this.state.xIsNext ? "X" : "O"}`; } return ( <div class> <Board squares={current.squares} onClick={(i) => { this.handleClick(i); }} /> </div> <div class> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); }}// eslint-disable-next-line react/no-deprecatedReact.render(<App />, document.getElementById("root"));

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

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/SzrEj3YFJwk6rT?from=pc

END

成就一亿技术人

https://p26.toutiaoimg.com/origin/tos-cn-i-tjoges91tu/So9tIFGG7pUPKG?from=pc