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