【CSDN 編者按】400行代碼在React 18中實現(xiàn)可中斷的異步更新的最小模型!
原文鏈接:https://betterpr
ogramming.pub/react-18-has-been-released-implement-mini-react-in-400-lines-of-code-837559761758
聲明:本文為 CSDN 翻譯,轉(zhuǎn)載請注明來源。
作者 | Zachary Lee 譯者 | 彭慧中
責編 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下為譯文:
React v18已經(jīng)發(fā)布,它給我們帶來了許多特性,但最重要的特性是可中斷的異步更新,許多新的上層API都是通過它創(chuàng)建的??梢哉f,它是React v18的底層引擎。
本文將使用大約400行代碼帶你實現(xiàn)一個可以異步更新和可中斷更新的Mini-React,如下圖所示:
我使用了React官方網(wǎng)站提供的tic-tac-toe教程示例(以下是鏈接:https://reactjs.org/tutorial/tutorial.html#what-are-we-building),可以看到它非常有效。此外,它目前支持函數(shù)組件和類組件,可以滿足開發(fā)者80%的需求!我也把它放在GitHub上(以下是鏈接:https://github.com/islizeqiang/mini-react),你也可以在本地復制它,并按照我的文章一步一步地調(diào)試。
這是我在閱讀了大量React的源代碼后創(chuàng)建的,在整體邏輯和函數(shù)命名上基本上和React一樣,如果你對React的內(nèi)部原理感興趣,這篇文章就是為你準備的!
JSX和createEelement
我相信你對 React 中的 JSX 并不陌生。我們使用 JSX 來描述 DOM,它們最終會被 babel 轉(zhuǎn)換成 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);
你還可以在編譯好的字符串中加入更多的元素,再看看最終的結(jié)果,我在這里直接給出React.createElement提供的選項。
1.type:表示當前節(jié)點的類型,如上圖中的div。
2.config:表示當前元素節(jié)點上的屬性,如上圖中的{id: \”test\”}。
3.children:子元素,可以是多個、簡單的文本,也可以由React.createElement創(chuàng)建的子節(jié)點。
然后根據(jù)這個要求實現(xiàn)你自己的React.createElement,就像下面的代碼一樣,我們定義一個自定義的數(shù)據(jù)結(jié)構(gòu)。
渲染
然后我們可以根據(jù)上面創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)一個簡化版的渲染函數(shù),將JSX渲染到真實的DOM上。
下面的代碼演示將使用CodeSandbox,拖動左欄查看代碼,點擊上方的菜單按鈕查看目錄結(jié)構(gòu)。你也可以直接編輯,查看顯示的結(jié)果。
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\"));
所以你可以看到它工作,但現(xiàn)在它只渲染一次,不能與我們互動。
另外,請注意我們在這里使用react-scripts@3.4.4來幫助編譯JSX,API在以后的版本中已經(jīng)發(fā)生了變化,但是React.createElement在結(jié)束時仍然被調(diào)用。我提供的GitHub庫使用了Vite而不是react-scripts。
接下來,是React的核心纖程架構(gòu)和并發(fā)模式,這是從React 17開始提出的,主要是為了解決一旦完整的元素樹被遞歸,就無法終止的問題,這可能導致主線程長時間被阻塞,那些高優(yōu)先級的任務(比如那些用戶輸入或動畫等)無法及時處理。
所以在React源代碼中,工作被分解成小單元。一旦瀏覽器處于空閑狀態(tài),它將處理這些小的工作單元,然后將結(jié)果映射到實際的DOM,直到所有結(jié)果都被處理完。
requestIdleCallback是一個實驗性API,它在瀏覽器空閑時執(zhí)行回調(diào)。接下來,我們將使用這個API來簡單地實現(xiàn)這個功能。我將在最后給出React目前使用的調(diào)度程序包的模擬實現(xiàn)。
在開始編寫下一個代碼之前,我想再次介紹一下工作單元之間的連接。
就像上面的圖片一樣,我們將像鏈表一樣創(chuàng)建每個纖程節(jié)點之間的連接,它們是:
1.child:父節(jié)點指向第一個子元素的指針。
2.return/parent:所有子元素都有一個指向父元素的指針。
3.sibling:從第一個子元素指向下一個同級元素。
所以現(xiàn)在你可以愉快地編寫代碼:
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\"));
盡管添加了這么多代碼,我們只是重構(gòu)了渲染邏輯。重構(gòu)后的調(diào)用順序為workLoop →performUnitOfWork→reconcileChildren。下面讓我來總結(jié)一下各個功能的作用:
1.workLoop:通過連續(xù)調(diào)用requestIdleCallback來獲得空閑時間。如果當前空閑且有單元任務要執(zhí)行,則執(zhí)行每個單元任務。
2.performUnitOfWork:執(zhí)行的特定單元任務。這是鏈表思想的體現(xiàn)。即一次只處理一個纖程節(jié)點,并返回下一個要處理的節(jié)點。
3.reconcileChildren:協(xié)調(diào)當前纖程節(jié)點,它實際上是虛擬DOM的比較,并記錄要進行的更改。你可以看到,我們直接在每個纖程節(jié)點上修改和保存,因為現(xiàn)在它只是對JavaScript對象的修改,而不涉及真正的DOM。
4.最后一步是commitRoot。如果當前需要更新(根據(jù)wipRoot),并且沒有下一個單元任務要處理(根據(jù)!nextUnitOfWork),這意味著需要將虛擬更改映射到實際的DOM。commitRoot負責根據(jù)纖程節(jié)點的變化修改真實的DOM。
到目前為止,我們已經(jīng)實現(xiàn)了纖程架構(gòu),是時候見證它的威力了。
我們想給組件添加狀態(tài),讓我們實現(xiàn)一個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}
</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()
}
],
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巧妙地將hook的狀態(tài)保留在纖程節(jié)點上,并通過隊列修改狀態(tài)。從這里,我們也可以知道為什么React-hooks要求每次調(diào)用的順序不能改變。
除此以外,我們還實現(xiàn)了一個Component ,這里只是簡單地轉(zhuǎn)換為一個渲染的方法,并添加了一點它的獨特身份。
模擬requestIdleCallback
現(xiàn)在我們幾乎實現(xiàn)了所有的功能,讓我解釋一下React目前采用的調(diào)度器包,它實際上是一個比requestIdleCallback更復雜的調(diào)度邏輯,包括更新任務的優(yōu)先級等等。
上面是我實現(xiàn)模擬requestIdleCallback的參考調(diào)度程序,它結(jié)合了requestAnimationFrame和MessageChannel。這里使用MessageChannel的目的是使用宏任務來處理每一輪的單元任務。
那么為什么要使用宏任務呢?
為了放棄主線程,瀏覽器可以在空閑期間更新DOM或接收事件。因為瀏覽器更新DOM是一個獨立的任務,而JavaScript在這個時候不會被執(zhí)行,因為主線程一次只能運行一個功能,要么執(zhí)行JS,要么處理DOM計算樣式,要么接收輸入事件,等等。
為什么不使用微任務呢?
因為微任務包含在每一輪宏任務中,所以在所有微任務執(zhí)行完畢之前,也就是當前宏任務未完成時,主線程不能放棄。
為什么不使用setTimeout呢?
因為如果setTimeout被嵌套調(diào)用超過5次,該函數(shù)將被視為阻塞,瀏覽器將把最小時間設置為4ms,所以它不夠精確。
最終版本
下面是最終的版本,你可以看到,在去掉注釋后,不到400行代碼就實現(xiàn)了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}
</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()
}
],
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\"));
我還在GitHub中添加了一個TypeScript版本的Mini-React(https://github.com/islizeqiang/mini-react/blob/master/src/mini-react.ts),如果你有興趣,可以去看看。
END
成就一億技術人
版權聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。