リード:React 18で割り込み可能な非同期更新の最小モデルを実装するための400行のコード!
元のリンク:https
://betterprogramming.pub/react-18-has-been-released-implement-mini-react-in-400-lines-of-code-837559761758ステートメント:この記事はCSDNによって翻訳されています。再版のソース。
以下は翻訳です:
React v18がリリースされ、多くの機能が提供されていますが、最も重要な機能は、多くの新しい上位レベルのAPIが作成される割り込み可能な非同期更新です。Reactv18の基盤となるエンジンと言えます。
この記事では、次の図に示すように、約400行のコードを使用して、非同期および割り込み可能な更新を更新できるMini-Reactを実装します
。Reactの公式Webサイトで提供されている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 = <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を実装します。以下のコードのように、カスタムデータ構造を定義します。
与える
次に、上記で作成したデータ構造に基づいて簡略化されたレンダリング関数を実装して、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"));
だからあなたはそれが機能しているのを見ることができますが、今では一度だけレンダリングされ、私たちと対話することはできません。
また、ここではJSXのコンパイルを支援するために[email protected]を使用していることに注意してください。APIは後のバージョンで変更されていますが、最後にReact.createElementが呼び出されます。私が提供したGitHubリポジトリは、react-scriptsの代わりにViteを使用しています。
次に、React 17以降に提案されたReactのコアファイバーアーキテクチャと同時実行モードです。主に、要素ツリー全体が繰り返されると終了できないため、メインスレッドが長時間ブロックされる可能性があるという問題を解決します。時間の経過とともに、これらの優先度の高いタスク(ユーザー入力やアニメーションなど)をタイムリーに処理することはできません。
そのため、Reactソースコードでは、作業は小さな単位に分割されます。ブラウザがアイドル状態になると、これらの小さな作業単位が処理され、すべての結果が処理されるまで、結果が実際のDOMにマップされます。
requestIdleCallbackは、ブラウザがアイドル状態のときにコールバックを実行する実験的なAPIです。次に、このAPIを使用して、この機能を簡単に実装します。最後に、Reactで現在使用されているディスパッチャーパッケージの模擬実装を示します。
次のコードを書き始める前に、作業単位間の接続をもう一度紹介したいと思います。
上の図のように、リンクリストのように各ファイバーノード間の接続を作成します。それらは次のとおりです。
1.child:最初の子要素への親ノードのポインター。
2.return / parent:すべての子要素には親要素へのポインターがあります。
3.兄弟:最初の子要素から次の兄弟要素まで。
これで、楽しくコーディングできます。
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:実行された特定のユニットタスク。これは、リンクリストのアイデアの具体化です。つまり、一度に1つのファイバーノードのみを処理し、次のノードを処理に戻します。
3.reconcileChildren:現在のファイバーノードを調整します。これは実際には仮想DOMの比較であり、行われる変更を記録します。ご覧のとおり、実際のDOMではなく、JavaScriptオブジェクトに対する単なる変更であるため、各ファイバーノードを直接変更して保存します。
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 null;
}
class Square extends React.Component {
render() {
return (
<button onClick={this.props.onClick} className="square">
{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 className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{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(null)
}
],
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 className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => {
this.handleClick(i);
}}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
// eslint-disable-next-line react/no-deprecated
React.render(<App />, document.getElementById("root"));
useStateは、ファイバーノードのフックの状態を巧みに維持し、キューを介して状態を変更します。ここから、React-hookが各呼び出しの順序を変更できないようにする必要がある理由もわかります。
これに加えて、コンポーネントも実装します。これは、ここでは単にレンダリングメソッドに変換され、その一意のIDを少し追加します。
モックrequestIdleCallback
ほぼすべての機能が実装されたので、Reactで現在採用されているスケジューラパッケージについて説明します。これは、タスクの優先度の更新など、実際にはrequestIdleCallbackよりも複雑なスケジューリングロジックです。
上記は、requestAnimationFrameとMessageChannelを組み合わせた模擬requestIdleCallbackを実装する私の参照スケジューラです。ここでMessageChannelを使用する目的は、マクロタスクを使用してユニットタスクの各ラウンドを処理することです。
では、なぜマクロタスクを使用するのでしょうか。
メインスレッドを放棄するために、ブラウザーはDOMを更新したり、アイドル期間中にイベントを受信したりできます。ブラウザによるDOMの更新は別のタスクであり、JavaScriptは現時点では実行されないため、メインスレッドは、JSの実行、DOM計算スタイルの処理、入力イベントの受信など、一度に1つの関数しか実行できないためです。 。
マイクロタスクを使用しないのはなぜですか?
マイクロタスクはマクロタスクの各ラウンドに含まれているため、すべてのマイクロタスクが実行される前、つまり現在のマクロタスクが完了していない場合、メインスレッドはあきらめることができません。
setTimeoutを使用しないのはなぜですか?
setTimeoutが5回以上ネストされて呼び出された場合、関数はブロックされていると見なされ、ブラウザーは最小時間を4msに設定するため、十分に正確ではありません。
最終バージョン
以下は最終バージョンです。Reactのコアアイデアは、コメントを外した後、400行未満のコードで実装されていることがわかります。
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 null;
}
class Square extends React.Component {
render() {
return (
<button onClick={this.props.onClick} className="square">
{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 className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{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(null)
}
],
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 className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => {
this.handleClick(i);
}}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
// eslint-disable-next-line react/no-deprecated
React.render(<App />, document.getElementById("root"));
また、TypeScriptバージョンのMini-ReactをGitHub(https://github.com/islizeqiang/mini-react/blob/master/src/mini-react.ts)に追加しました。興味がある場合は、次のことができます。見てください。