性能主要取决于代码的作用,而不是选择函数式还是类式
本篇文章不是对函数式或者类式组件的价值判断,只是在阐述React
中这两种编程模型之间的区别,关于更多的函数式问题,请查看Hooks FAQ
函数式组件捕捉了渲染所用的值
思考以下组件
const FnCpm: FC<Props> = (props) => {
const myAlert = () => {
alert("look here!" + props.user);
};
const handler = () => {
setTimeout(myAlert, 3000);
};
return <button onClick={handler}>function Click</button>;
};
它渲染了一个利用setTimeout
来模拟网络请求,然后显示一个确认警告的按钮。例如,如果props.user
是Qsj
,它会在三秒后显示 look here! Qsj
,非常简单
如果是一个class component
class ClassComponent extends Component<Props> {
myAlert = () => {
alert("look here!" + this.props.user);
};
handler = () => {
setTimeout(this.myAlert, 3000);
};
render() {
return <button onClick={this.handler}>Class Click</button>;
}
}
通常我们认为,这两段代码是等价的,人们经常在这两种模式中自由的重构代码,但是很少注意到他们的含义
然而这两个代码片段还是有略微的不同,仔细的看看他们。
尝试按照以下顺序分别使用这两个按钮
- 点击 其中某一个按钮
- 在三秒内切换选中的
select
- 查看弹出的文本
你将看到一个奇特的区别
- 当使用
function component
,当前user
是张三
时点击按钮后,马上切换到李四
,弹出的文本将依旧是look here 张
- 当使用
class component
,当前user
是李四
时点击按钮,马上切换到张三
,弹出的确实look here 张三
在这个例子中,第一个行为是正确的。如果我关注一个人,然后切换到另外一个人的账号,我的组件不应该混淆我关注了谁。在这里,class
组件的实现很明显是错误的
why?
class ClassComponent extends Component<Props> {
myAlert = () => {
alert("look here!" + this.props.user) // look here
};
这个class
方法从this.props.user
中读取数据。在react
中props
是不可变的(immutable
)的,所以他们永远不会改变。然而,this
是,而且永远是,可变的。
事实上,这就是class
组件this
存在的意义。**react
本身会随着时间的推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例。**
所以如果在请求已经发出的情况下我们的组件进行了重新渲染,this.props
将会改变。myAlert
方法从一个“过于新”的props
中得到了user
。
这暴露了一个关于用户界面性质的一个有趣观察。如果我们说ui
在概念上是当前应用状态的一个函数,那么事件处理程序是渲染结果的一部分———就像视觉输出一样。我们的事件处理程序“属于”一个拥有特定props
和state
的特定渲染。
然而,调用一个回调函数读取this.props
的timeout
会打断这种关联。我们的myAlert
函数并没有与任何一个特定的渲染“绑定”在一起,所以它失去了正确的props
。从this
中读取数据这种行为,切断了这种联系
让我们假设函数组件不存在。我们该如何解决类组件这个问题?
我们想要从某种关系“修复”拥有正确的props
的渲染与读取这些props
的myAlert
回调之间的关系。在某个地方props
会被弄丢。
一种方法是在调用事件之前读取this.props
,然后将它们显示的传递给timeout
回调函数中去:
class ClassComponent extends Component<Props> {
myAlert = (user: string) => {
alert("look here!" + user);
};
handler = () => {
const { user } = this.props;
setTimeout(() => this.myAlert(user), 3000);
};
}
这种方法会起作用。然而,这种方法使得代码明显变的更加冗长,并且随着时间推移容易出错。如果我们需要的不止是一个props
怎么办?如果我们还需要访问一个state
怎么办?如果myAlert
中调用了另一个方法,然后那个方法读取了this.props.something
或者this.state.something
,我们又将会遇到同样的问题,然后我们不得不将this.props
和this.state
以函数参数的形式传递下去。
这样破坏了class
提供的工程学。同时这也很难让人去记住传递的变量,这就是为什么人们总是在解决bug。
同样的,在handler
中内联alert
代码也无法解决问题,我们希望以允许将拆分多个方法的方式去构造组织代码,但同时也能读取与某次组件调用形成的渲染结果对应的props
和state
。这种问题并不是react
所独有的————你可以在任何一个将数据放入类似this
这样的可变对象中的ui
库中重现它。
我们面对的问题是我们从this.props
中读取数据太迟了——读取时已经不是我们所需要使用的上下文了!然而如果我们利用javascript
函数中的闭包问题将迎刃而解
class ClassComponent extends Component<Props> {
render() {
const { user } = this.props;
const myAlert = (user: string) => {
alert("look here!" + user);
};
const handler = () => {
setTimeout(() => myAlert(user), 3000);
};
return <button onClick={handler}>Class Click</button>;
}
}
通常来说我们会避免使用闭包,因为它会让我们难以想象一个可能会随着时间推移而变化的变量。(但是在react
中。props
和state
是不可变的)或者说,react
强烈推荐是不可变的。这就消除了闭包的主要缺陷
这就意味着如果你在一次特定的渲染中捕捉那一次渲染所用的props
或者state
,你会发现她们总是保持一致,就如同你预期的那样
你在渲染的时候就已经”捕捉“了props
这样,在它内部的任何代码(包括myAlert
)都保证可以得到这一次特定渲染所使用的props
,react
再也不会动我们的奶酪
用闭包是正确的,但是看起来很奇怪,如果你在render
方法中定义各种函数,而不是使用class
的方法,那么class
的意义又在哪里?
事实上,我们可以通过删除class
的”包裹“来简化代码:
const FnCpm: FC<Props> = (props) => {
const myAlert = () => {
alert("look here!" + props.user);
};
const handler = () => {
setTimeout(myAlert, 3000);
};
return <button onClick={handler}>function Click</button>;
};
就像上面这样,props
仍旧被捕获—— react
会将它们作为参数传递。不同于this
,props
对象本身永远都不会被react
改变
如果你在函数定义中解构props
,那更加明显
const FnCpm: FC<Props> = ({ user }) => {
const myAlert = () => {
alert("look here!" + user);
};
};
当父组件使用不同的props
来渲染function component
时,react
会再次调用functiom component
,但是我们点击的事件处理函数,”属于“具有自己的user
值的上一次渲染,并且myAlert
回调函数也会读取到这个值,它们都会完好无损。
这就是为什么在function component
中,当前user
为张三,点击后改变为李四,三秒后alert
仍然会弹出张三的原因,这个行为是正确的
现在明白了react
中function component
和class component
之间的区别:
函数式组件捕捉了渲染所使用的值
使用hooks
,同样的原则也适用于state
function MessageThread() {
const [message, setMessage] = useState("");
const showMessage = () => {
alert("You said: " + message);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e: any) => {
setMessage(e.target.value);
};
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
尽管这不是一个非常好的fn component
,但它说明了同样的观点:如果我发送一条特定的消息,组件不应该对实际发送的是哪条消息感到困惑。这个函数组件的message
变量捕获了“属于”返回了被浏览器调用的单击处理函数的那一次渲染。所以但我点击“发送”的时候,message
被设置为那一刻在input
输入的内容
因此我们知道,在默认情况下react
中的函数会捕获props
和state
,但是如果我们不想要读取并不属于这一次特定渲染的,最新的props
和state
呢?如果我们想要从未来读取它们呢?
在class
中,你通过读取this.props
或者this.state
来实现,因为this
本身就是可变的,react
改变了它,在fn component
中,你也可以拥有一个在所有的渲染帧中共享的可变变量,它被称为“ref
“
function MyComponent() {
const ref = useRef(null);
}
但是,你必须自己管理它
你可能知道“dom ref
”,但是ref
在概念上更为广泛通用。它只是一个你可以放东西进去的盒子。
甚至在视觉上,this.something
就像是something.current
的一个镜像,它们代表同样的概念
默认情况下,react
不会在fn component
中为最新props
和state
创造ref
,在很多情况下,你并不需要它们,并且分配它们将是一种浪费,但是如果你愿意,你可以这样手动的追踪这些值:
function MessageThread() {
const [message, setMessage] = useState("");
const latestMessage = useRef("");
const showMessage = () => {
alert("You said: " + latestMessage.current); // edit
};
const handleMessageChange = (e: any) => {
setMessage(e.target.value);
latestMessage.current = e.target.value; // edit
};
}
当我们读取latestMessage.current
,我们将会得到最新的值,即使我们在按下按钮后继续输入
通常情况下,应该避免在渲染期间读取或者设置ref
,因为它们是可变的,so
,尽量应该保持渲染的可预测性,然而,如果我们想要特定props
或state
的最新值,那么手动更新ref
会有些烦人,我们可以通过effect
来自动化实现它:
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
我们在一个effect
内部执行赋值操作以便让ref
的值只会在dom
被更新后才会改变。这确保了我们的变量突变不会破坏依赖于中断渲染的时间切片和Suspense等特性
通常来说使用这样的ref
并不是非常的必要,捕捉props
和state
通常是更好的默认值。然而在处理类似于intervals
的命令式api
时,ref
会非常便利。记住你可以像这样追踪任何一个值——一个props
,一个state
,又或者甚至是一个函数
。
当用函数来编写大部分react
代码时,我们需要调整关于优化代码和什么变量会随着时间改变的认知和直觉。
目前为止,我发现的有关于hooks的最好的心智模型是“写代码时要认为任何值都可以随时改变”
函数也不例外。这需要一段时间才能成为react学习资料中的常识,它需要一些从class的思维方式中进行一些调整。但我希望这篇文章能够帮助你以新的目光来看待它。