一篇對
Dan
的 How Are Function Components Different from Classes? 一文的個人閱讀總結,內容來自於此。強烈推薦閱讀 Dan Abramov.的部落格。
函式式元件和Class元件有什麼不同?
Dan
很直接的給出了答案:
函式式元件捕獲了渲染所用的值。(Function components capture the rendered values.)
直接看結論可能有點不知所云。
class
元件可能引發的"錯誤"
看一個元件,使用setTimeout
模擬網路請求,點選button
之後警告提示關注某人(user
),user
從props
中讀取。
該元件的function
版本:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
class
版本:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
頁面元件程式碼:
class App extends React.Component {
state = {
user: 'Dan',
};
render() {
return (
<>
<label>
<b>Choose profile to view: </b>
<select
value={this.state.user}
onChange={e => this.setState({ user: e.target.value })}
>
<option value="Dan">Dan</option>
<option value="Sophie">Sophie</option>
<option value="Sunil">Sunil</option>
</select>
</label>
<h1>Welcome to {this.state.user}’s profile!</h1>
<p>
<ProfilePageFunction user={this.state.user} />
<b> (function)</b>
</p>
<p>
<ProfilePageClass user={this.state.user} />
<b> (class)</b>
</p>
<p>
Can you spot the difference in the behavior?
</p>
</>
)
}
}
對於class
元件,在選中狀態是userA
的時候,點選follow button
之後立馬將select
切換其他人(uerB
),三秒之後的彈出框是follow
的userB
。(這個動作會在後面多次提及)
在選中userA
的時候點選關注,目的就是關注userA
,但是class
元件最後彈出框顯示的關注userB
,這顯然不符合預期。
為什麼、如何解決
如果上面例子使用function
元件,彈出框顯示的就會是正確的,雖然切換到了選中的userB
,但是彈出框顯示的仍然是點選的那一刻關注的userA
。
function
元件好像記下了點選那一刻時候的狀態?
class
元件在這個場景中錯誤的原因是class
元件每次三秒後從this.props.user
中讀取資料,此時的this.props.user
已經變了,已經是切換後的新的this.props.user
資料。雖然React中props
不可變,但是this
是可變的。
類元件會隨著時間推移改變,在渲染方法和生命週期方法中得到的是最新的例項。而函式式元件的事件處理程式就是渲染結果的一部分,事件處理程式屬於一個擁有特定的props
和state
的渲染。
也就是說函式式元件保持了事件處理程式與那一次渲染props
的state
之間的聯絡,本身就是正確的。
來修復類元件中的這個問題:
-
可以在點選的時候就讀取並記錄當下的
state
或props
,三秒後讀取記錄的資料(而不是讀this.props.xx
)再彈出。方案可行但是擴充套件極差,在其他多個變數也這樣做的時候逐層記錄或傳遞非常繁複。
-
閉包。
閉包維持了一個可能隨時間變化的變數,而此處我們要維持的是React的
props
或state
,React設計中這都是不可變的。讓閉包來維持不變的state
和props
,此時再去捕獲這些值,就是一致的。在
render
函式中使用閉包:class ProfilePage extends React.Component { render() { // Capture the props! const props = this.props; // Note: we are *inside render*. // These aren't class methods. const showMessage = () => { alert('Followed ' + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return <button onClick={handleClick}>Follow</button>; } }
渲染的時候這些需要使用的
props
已經被捕獲(就像上面方案1
的記錄,在render
的時候就已經讀取記錄下了)。此時表現彈出內容就會是點選時候的那個userA
了。
class
元件的這個問題是修復了,但是在render
函式中新增那麼多的函式,且並沒有掛載到class
上,有點奇怪?
其實去掉class
,這就是函式式元件的形式了:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
React
將他們作為引數傳遞,props
在渲染時被捕獲了。不同於class
元件的this
,這裡的props
不會被改變。
點選事件處理函式,該函式屬於具有正確user
值的一次渲染,事件處理函式和其他回撥函式也能讀到這個值。
回頭看這個結論,是不是更好理解一點了:
函式式元件捕獲了渲染所使用的值
函式式元件使用最新的props
和state
函式式元件捕獲了特定渲染的props
和state
。但是我們如果又想和class
元件一樣讀取最新的props
和state
呢?
useRef
Dan 老師:在函式式元件中,你也可以擁有一個在所有的元件渲染幀中共享的可變變數。它被成為“ref”
this.something
就像是something.current
的一個映象。他們代表了同樣的概念。
每一次的渲染結果可以視為一個渲染幀,共享的變數設定為ref
,包含DOMRef
和class
中的例項變數的功能,可以說是非常強大了。
需要最新的props
和state
值,可以使用useRef
建立的變數來記錄,通過useEffect
可以在值變化的時候自動追蹤。
function MessageThread() {
const [message, setMessage] = useState('');
// 保持追蹤最新的值。
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
React函式總是捕獲他們的值。