目录
- 一、函数式组件捕获了渲染所用的值
- 二、闭包让类组件成为拥有特定props和state的渲染
- 三、区分useState与useRef的使用
首先我们要知道的是,项目性能能主要取决于代码的作用,而不是选择函数式还是类组件。尽管优化策略各有略微不同,但它们之间的性能差异可以忽略不计。
一、函数式组件捕获了渲染所用的值
首先我们来看下面这个组件:
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);
};
这个类方法从中读取数据。在 React 中 Props 是不可变的,所以他们永远不会改变。然而,this是,而且永远是,可变的。
事实上,这就是类组件存在的意义。React本身会随着时间的推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例。所以如果在请求已经发出的情况下我们的组件进行了重新渲染,将会改变。
我们的组件属于一个拥有特定 props 和 state 的特定渲染。
然而,调用一个回调函数读取 的 timeout 会打断这种关联。我们的回调并没有与任何一个特定的渲染绑定在一起,所以它失去了正确的 props。
二、闭包让类组件成为拥有特定props和state的渲染
我们想要以某种方式“修复”拥有正确 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。这样,在它内部的任何代码(包括)都保证可以得到这一次特定渲染所使用的props。上面的例子是正确的,但是看起来很奇怪。如果你在方法中定义各种函数,而不是使用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函数总是捕获他们的值 —— 现在我们也知道这是为什么了。