本文深入解析React函数组件与类组件的本质区别。重点阐述函数组件通过闭包捕获渲染时的Props和State,确保回调函数读取的是特定时刻的值;而类组件中this始终可变,导致异步回调可能读取到最新实例而非渲染时的状态。同时对比了useState与useRef在获取最新值与快照值上的不同应用场景,帮助开发者理解React渲染机制并编写更健壮的代码。
项目性能主要取决于代码逻辑本身,而非选择函数式组件还是类组件。尽管两者的优化策略存在细微差异,但实际性能差距微乎其微,可以忽略不计。
一、函数式组件捕获渲染时的值
首先观察以下组件示例:
function App(props) {
const showMessage = () => {
alert('Hello' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Say</button>
);
}该组件渲染一个按钮,点击后模拟网络请求并显示确认警告。例如,若传入的 props.user 为 'jie',三秒后将弹出 'Hello jie' 的提示。
若使用类组件实现相同功能,简单的重构可能如下所示:
class App extends React.Component {
showMessage = () => {
alert('Hello' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Say</button>;
}
}在代码重构时,我们常认为两者等效,但往往忽视其背后的语义差异。
接下来新建一个 React 项目,在 src 目录下创建 classComponent 和 functionComponent 两个组件。代码基于前文示例,略有调整:
classComponent 实现如下:
import React from 'react';
class ProfilePage extends React.Component {
showMessage = () => {
alert('你选择了 ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>选择</button>;
}
}
export default ProfilePage;functionComponent 实现如下:
import React from 'react';
function ProfilePage(props) {
const showMessage = () => {
alert('你选择了 ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>选择</button>
);
}
export default ProfilePage;在 app.js 中引入这两个组件:
import React from "react";
import ReactDOM from "react-dom";
import ProfilePageFunction from './functionComponent';
import ProfilePageClass from './classComponent';
export default class App extends React.Component {
state = {
user: '小杰',
};
render() {
return (
<>
<label>
<b>选择你想要拜访的朋友</b>
<select
value={this.state.user}
onChange={e => this.setState({ user: e.target.value })}
>
<option value="小杰">小杰</option>
<option value="小尚">小尚</option>
<option value="小宁">小宁</option>
</select>
</label>
<h1>欢迎来到 {this.state.user}的 家!</h1>
<p>
<ProfilePageFunction user={this.state.user} />
<b> (这是来自函数式组件的)</b>
</p>
<p>
<ProfilePageClass user={this.state.user} />
<b> (这是来自类组件的)</b>
</p>
</>
)
}
}运行项目后,界面显示如下:

点击上方按钮执行函数式组件逻辑,点击下方按钮执行类组件逻辑。按以往经验,两者结果应一致,但事实是否如此?
请按以下步骤操作:
1. 点击函数式组件的按钮
2. 点击后立即切换要访问的朋友
函数式组件的执行结果如下:

页面弹出的仍是点击按钮时选择的值。
对类组件执行相同操作:

此时页面弹出的是实时更改后的值。
在此例中,函数式组件的行为是正确的。最初选择访问小杰并点击确定,随后切换至小尚但未再次点击确定,组件不应混淆访问对象。类组件的实现显然存在错误。
那么,为何类组件会出现这种表现?
仔细查看类组件中的方法:showMessage
showMessage = () => {
alert('你选择了 ' + this.props.user);
};该方法从 this 中读取数据。在 React 中,Props 是不可变的,因此它们不会改变。然而,this 是可变的,且始终可变。
这正是类组件的设计意图:React 会随时间更新实例,以便在渲染方法及生命周期方法中获取最新实例。因此,若组件在请求发出后重新渲染,this 会发生变化。
我们的组件属于拥有特定 props 和 state 的特定渲染实例。
然而,调用回调函数读取 this 的 timeout 打破了这种关联。该回调并未绑定到任何特定渲染,因此丢失了正确的 props。
二、闭包使类组件成为拥有特定 props 和 state 的渲染
我们需要以某种方式“修复”拥有正确 props 的渲染与读取这些 props 的回调之间的联系。它们在类的实现中丢失了。
一种方法是在调用事件前读取 props,并显式传递到 timeout 回调函数中:
import React from 'react';
class ProfilePage extends React.Component {
showMessage = (user) => {
alert('你选择了 ' + user);
};
handleClick = () => {
const {user} = this.props;
setTimeout(() => this.showMessage(user), 3000);
};
render() {
return <button onClick={this.handleClick}>确定</button>;
}
}
export default ProfilePage;这种方法有效,但使代码变得冗长且易出错。如果需要访问多个 props 或 state 怎么办?
若能利用 JavaScript 闭包,问题将迎刃而解。
通常我们避免使用闭包,但在 React 中,props 和 state 是不可变的,这消除了闭包的一个主要缺陷。
这意味着,若在特定渲染中捕获该次渲染所用的 props 或 state,它们将始终保持一致,符合预期。
class ProfilePage extends React.Component {
render() {
const props = this.props;
const showMessage = () => {
alert('你选择了 ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>确定</button>;
}
}在渲染时已“捕获” props。因此,其内部任何代码(包括 setTimeout)都能保证获取该特定渲染所使用的 props。上述示例逻辑正确,但写法奇特。若在 render 方法中定义各种函数而非使用 class 方法,使用类的意义何在?
此时我们明确了函数式组件与类组件的区别:
function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}当父组件使用不同 props 渲染时,React 会再次调用函数。但点击事件处理函数“属于”具有自身值的上一次渲染,回调函数也能读取该值。它们保持完好。
三、区分 useState 与 useRef 的使用
使用 Hooks 时,相同原则适用于状态。看此例:
function MessageThread() {
const [message, setMessage] = useState('');
const showMessage = () => {
alert('You said: ' + message);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e) => {
setMessage(e.target.value);
};
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}若发送特定消息,组件不应混淆实际发送的是哪条消息。该函数组件的变量捕获了浏览器中执行单击处理函数的那次渲染。因此,点击“发送”时,输入框内容即被设为弹出值。
由此可知,默认情况下 React 函数会捕获 props 和 state。但若需读取不属于此次特定渲染的最新 props 和 state 呢?
在函数式组件中,也可拥有在所有组件渲染帧中共享的可变变量,称为“ref”:
function MyComponent() {
const ref = useRef(null);
// 你可以通过 ref.current 来获取保存的值.
// ...
}多数情况下无需使用它们,分配它们是一种浪费。但若愿意,可手动追踪这些值:
function MessageThread() {
const [message, setMessage] = useState('');
const latestMessage = useRef('');
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e) => {
setMessage(e.target.value);
latestMessage.current = e.target.value;
};若在 state 中读取,将得到按下发送按钮那一刻的信息。但若通过 ref 读取,即使按下按钮后继续输入,也将得到最新值。
通常应避免在渲染期间读取或设置 refs,因为它们是可变的。我们希望保持渲染的可预测性。但若需特定 props 或 state 的最新值,手动更新 ref 较为繁琐。可通过 effect 自动化实现:
function MessageThread() {
const [message, setMessage] = useState('');
// 保持追踪最新的值。
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};如上所示,闭包实际上解决了难以察觉的细微问题。同时,它们也使在并发模式下编写正确运行的代码更加轻松。这是因为组件内部逻辑在渲染时捕获并包含了正确的 props 和 state。
React 函数总是捕获它们的值——现在我们也知道原因了。
到此这篇关于 React 函数组件与类的区别有哪些的文章就介绍到这了,更多相关 React 函数组件与类内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!