一篇對
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函數總是捕獲他們的值。
