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