参考文章:
文章在一开始就声明了:这两种写法没有好坏之分,性能差距也几乎可以忽略,而且 React 会长期支持这两种写法。
至于代码的性能问题,这主要取决于代码的作用,而不是选择函数式组件和类组件
React的函数式组件和类组件之间是否有任何根本上的区别?当然有 —— 在心智模型上。
函数式组件捕获了渲染所用的值。(Function components capture the rendered values.)
现在有这么一段代码
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
它渲染了一个利用setTimeout
来模拟网络请求,然后显示一个确认警告的按钮。例如,如果props.user
是Dan
,它会在三秒后显示Followed Dan
。非常简单。
如果是类组件我们怎么写?一个简单的重构可能就象这样:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
这两个组件都描述了同一个逻辑:点击按钮 3 秒后 alert
父级传入的用户名。
那么当点击按钮后的 3 秒内,父级修改了 this.state.user
,弹出的用户名是修改前的还是修改后的呢?
你将看到一个非常奇怪的现象:
- Function Component 展示的是修改前的值:
- Class Component 展示的是修改后的值:
React 文档中描述的 props
不是不可变(Immutable) 数据吗?为啥在运行时还会发生变化呢?
原因在于,虽然 props
不可变,是 this
在 Class Component 中是可变的,因此 this.props
的调用会导致每次都访问最新的 props
。
而 Function Component 不存在 this.props
的语法,因此 props
总是不可变的。
Function Component:
function ProfilePage(props) {
setTimeout(() => {
// 就算父组件 reRender,这里拿到的 props 也是初始的
console.log(props);
}, 3000);
}
Class Component:
class ProfilePage extends React.Component {
render() {
setTimeout(() => {
// 如果父组件 reRender,this.props 拿到的永远是最新的。
// 并不是 props 变了,而是 this.props 指向了新的 props,旧的 props 找不到了
console.log(this.props);
}, 3000);
}
}
如果希望在 Class Component 捕获瞬时 Props,可以: const props = this.props;
,但这样的代码很蹩脚,所以如果希望拿到稳定的 props
,使用 Function Component 是更好的选择。
现在我们明白了React中函数式组件和类组件之间的巨大差别:
函数式组件捕获了渲染所使用的值。
使用Hooks,同样的原则也适用于state。
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>
</>
);
}
在点击 Send
按钮后,再次修改输入框的值,3 秒后的输出依然是 点击前输入框的值。这说明 Hooks 同样具有 capture value 的特性。
利用 useRef
可以规避 capture value 特性:
function MessageThread() {
const latestMessage = useRef("");
const showMessage = () => {
alert("You said: " + latestMessage.current);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = e => {
latestMessage.current = e.target.value;
};
}
只要将赋值与取值的对象变成 useRef
,而不是 useState
,就可以躲过 capture value 特性,在 3 秒后得到最新的值。
这说明了利用 Function Component + Hooks 可以实现 Class Component 做不到的 capture props、capture value,而且 React 官方也推荐 新的代码使用 Hooks 编写。
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);
};
我们在一个effect内部执行赋值操作以便让ref的值只会在DOM被更新后才会改变。这确保了我们的变量突变不会破坏依赖于可中断渲染的时间切片和 Suspense等特性。
通常来说使用这样的ref并不是非常地必要。捕获props和state通常是更好的默认值。然而,在处理类似于intervals和订阅这样的命令式API时,ref会十分便利。记住,你可以像这样跟踪任何值 —— 一个prop,一个state变量,整个props对象,或者甚至一个函数。
例如当useCallback
本身经常改变时。然而,使用一个reducer通常是一个更好的解决方式。
使用hooks的最好的心里规则是“写代码时要认为任何值都可以随时更改。