react类组件表示销毁阶段_[译]React函数组件和类组件的差异

[译]React函数组件和类组件的差异

原文: https://overreacted.io/how-are-function-components-different-from-classes/

在 React.js 开发中,函数组件(function component) 和 类组件(class component) 有什么差异呢?

在以前,通常认为区别是,类组件提供了更多的特性(比如state)。随着 React Hooks 的到来,这个说法也不成立了(通过hooks,函数组件也可以有state和类生命周期回调了)。

或许你也听说过,这两类组件中,有一类的性能更好。哪一类呢?很多这方面的性能测试,都是 有缺陷的 ,因此要从这些测试中 得出结论 ,不得不谨慎一点。性能主要取决于你代码要实现的功能(以及你的具体实现逻辑),和使用函数组件还是类组件,没什么关系。我们观察发现,尽管函数组件和类组件的性能优化策略 有些不同 ,但是他们性能上的差异是很微小的。

不管是上述哪个原因,我们都 不建议 你使用函数组件重写已有的类组件,除非你有别的原因,或者你喜欢当第一个吃螃蟹的人。React Hooks 还很新(就像2014年的React一样),目前还没有使用hooks相关的最佳实践。

除了上面这些,还有什么别的差异么?在函数组件和类组件之间,真的存在根本性的不同么?"Of course, there are — in the mental model" (这个实在不知道咋表达,贴下作者原文吧) 在这篇文章里,我们将一起看下,这两类组件最大的不同。这个不同点,在2015年函数组件函数组件 被引入React 时就存在了,但是被大多数人忽略了。

函数组件和类组件的差异

函数组件会捕获render内部的状态

让我们一步步来看下,这代表什么意思。

注意,本文并不是对函数组件和类组件进行价值判断。我只是展示下React生态里,这两种组件编程模型的不同点。要学习如何更好的采用函数组件,推荐官方文档 Hooks FAQ 。

假设我们有如下的函数组件:

function ProfilePage(props) { const showMessage = () => { alert('Followed ' + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return ( Follow );}

组件会渲染一个按钮,当点击按钮的时候,模拟了一个异步请求,并且在请求的回调函数里,显示一个弹窗。比如,如果 props.user 的值是 Dan,点击按钮3秒之后,我们将看到 Followed Dan 这个提示弹窗。非常简单。

(注意,上面代码里,使用箭头函数还是普通的函数,没有什么区别。因为没有 this 问题。把箭头函数换成普通函数 function handleClick()没有任何问题 )

我们怎样实现同样功能的类组件呢?很简单的翻译一下:

class ProfilePage extends React.Component { showMessage = () => { alert('Followed ' + this.props.user); }; handleClick = () => { setTimeout(this.showMessage, 3000); }; render() { return Follow; }}

通常情况下,我们会认为上面两个组件实现,是完全等价的。开发者经常像上面这样,在函数组件和类组件之间重构代码,却没有意识到他们隐含的差异。

fc3c5a7cf7997c790f9b7d4e62a10e70.png

但是,上面两种实现,存在着微妙的差异 。再仔细看一看,你能看出其中的差异么?讲真,还是花了我一段时间,才看出其中的差异。

如果你想在线看看源代码,你可以 点击这里 。 本文剩余部分,都是讲解这个不同点,以及为什么不同点会很重要。

在我们继续往下之前,我想再次说明下,本文提到的差异性,和react hooks本身完全没有关系!上面例子里,我都没用到hooks呢。

本文只是讲解,react生态里,函数组件和类组件的差异性。如果你打算在react开发中,大规模的使用函数组件,那么你可能需要了解这个差异。

我们将使用在react日常开发中,经常遇到的一个bug,来展示这个差异 。

接下来,我们就来复现下这个bug。打开 这个在线例子 ,页面上有一个名字下拉框,下面是两个关注组件,一个是前文的函数组件,另一个是类组件。

对每一个关注按钮,分别进行如下操作:

  1. 点击其中1个关注按钮在3秒之内,重新选择下拉框中的名字3秒之后,注意看alert弹窗中的文字差异

你应该注意到了两次alert弹窗的差别:

  • 在函数组件的测试情况下,下拉框中选中 Dan,点击关注按钮,迅速将下拉框切换到Sophie,3秒之后,alert弹窗内容仍然是 Followed Dan在类组件的测试情况下,重复相同的动作,3秒之后,alert弹窗将会显示 Followed Sophie
879c16c441534dce551850c802f2dac7.png

在这个例子里,使用函数组件的实现是正确的,类组件的实现明显有bug。如果我先关注了一个人,然后切换到了另一个人的页面,关注按钮不应该混淆我实际关注的是哪一个 。

(PS,我也推荐你真的关注下 Sophie)

那么,为什么我们的类组件,会存在问题呢?

让我们再仔细看看类组件的 showMessage 实现:

class ProfilePage extends React.Component { showMessage = () => { alert('Followed ' + this.props.user); };

这个方法会读取 this.props.user 。在React生态里,props是不可变数据,永远不会改变。但是,this却始终是可变的 。

确实,this存在的意义,就是可变的。react在执行过程中,会修改this上的数据,保证你能够在 render和其他的生命周期方法里,读取到最新的数据(props, state)。

因此,如果在网络请求处理过程中,我们的组件重新渲染,this.props改变了。在这之后,showMessage方法会读取到改变之后的 this.props。

这揭示了用户界面渲染的一个有趣的事实。如果我们认为,用户界面(UI)是对当前应用状态的一个可视化表达(UI=render(state)),那么事件处理函数,同样属于render结果的一部分,正如用户界面一样 。我们的事件处理函数,是属于事件触发时的render,以及那次render相关联的 props 和state。

然而,我们在按钮点击事件处理函数里,使用定时器(setTimeout)延迟调用 showMessage,打破了showMessage和this.props的关联。showMessage回调不再和任何的render绑定,同样丢失了本来关联的props。从 this 上读取数据,切断了这种关联。

假如函数组件不存在,那我们怎么来解决这个问题呢?

我们需要通过某种方式,修复showMessage和它所属的 render以及对应props的关联。

一种方式,我们可以在按钮点击处理函数中,读取当前的 props,然后显式的传给 showMessage,就像下面这样:

class ProfilePage extends React.Component { showMessage = (user) => { alert('Followed ' + user); }; handleClick = () => { const {user} = this.props; setTimeout(() => this.showMessage(user), 3000); }; render() { return Follow; }}

这种方式 可以解决这个问题 。然而,这个解决方式让我们引入了冗余的代码,随着时间推移,容易引入别的问题。如果我们的 showMessage方法要读取更多的props呢?如果showMessage还要访问state呢?如果 showMessage 调用了其他的方法,而那个方法读取了别的状态,比如this.props.something 或 this.state.something ,我们会再次面临同样的问题。 我们可能需要在 showMessage 里显式的传递 this.props this.state 给其他调用到的方法。

这样做,可能会破坏类组件带给我们的好处。这也很难判断,什么时候需要传递,什么时候不需要,进一步增加了引入bug的风险。

同样,简单地把所有代码都放在 onClick 处理函数里,会带给我们其他的问题。为了代码可读性、可维护性等原因,我们通常会把大的函数拆分为一些独立的小的函数。这个问题不仅仅是react才有,所有在this上维护可变数据的UI类库,都很容易遇到这个问题。

或许,我们可以在类的构造函数里绑定一些方法?

class ProfilePage extends React.Component { constructor(props) { super(props); this.showMessage = this.showMessage.bind(this); this.handleClick = this.handleClick.bind(this); } showMessage() { alert('Followed ' + this.props.user); } handleClick() { setTimeout(this.showMessage, 3000); } render() { return Follow; }}

很遗憾,上面的代码 不能 解决这个问题!记住,导致这个问题的原因,是我们读取 this.props的时机太晚了,和语法没有关系。然而,如果我们能够完全依赖JavaScript的闭包机制,那么就能彻底解决这个问题 。

我们大多数情况下,会尽量避免使用闭包,因为在闭包的情况下,判断一个可变的变量值,会变得 有些困难 。但是,在react里,props和state是 不可变的(严格来说,我们强烈推荐将props和state作为不可变数据)。props和state的不可变特性,完美解决了使用闭包带来的问题。

这意味着,如果在 render方法里,通过闭包来访问props和state,我们就能确保,在showMessage执行时,访问到的props和state就是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 Follow; }}

在render执行时,你成功的捕获了当时的props 。

0f9796796d8ce25e6fb0b447b303e6d6.png

通过这种方式,render方法里的任何代码,都能访问到render执行时的props,而不是后面被修改过的值。react不会再偷偷挪动我们的奶酪了。

像上面这样,我们可以在render方法里,根据需要添加任何帮助函数,这些函数都能够正确的访问到render执行时的props。闭包,这一切的救世主。

上面的代码,功能上没问题,但是看起来有点怪。如果组件逻辑都作为函数定义在render内部,而不是作为类的实例方法,那为什么还要用类呢?

确实,我们剥离掉类的外衣,剩下的就是一个函数组件:

function ProfilePage(props) { const showMessage = () => { alert('Followed ' + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return ( Follow );}

这个函数组件和上面的类组件一样,内部函数捕获了props,react会把props作为函数参数传进去。和this不同的是,props是不可变的,react不会修改props 。

如果你在函数参数里,把props结构,代码看起来会更加清晰:

function ProfilePage({ user }) { const showMessage = () => { alert('Followed ' + user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return ( Follow );}

当父组件传入不同的props来重新渲染 ProfilePage时,react会再次调用 ProfilePage。但是在这之前,我们点击关注按钮的事件处理函数,已经捕获了上一次render时的props。

这就是为什么,在 这个例子 的函数组件中,没有问题。

7917f92149911facf407f5cec3cbb82e.png

可以看到,功能完全是正确的。(再次PS,建议你也关注下 Sunil)

现在我们理解了,在函数组件和类组件之间的这个差异:

函数组件会捕获render内部的状态

函数组件配合React Hooks

在有 Hooks 的情况下,函数组件同样会捕获render内部的 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 ( <> Send > );}

(在线demo,点击这里)

尽管这是一个很简陋的消息发送组件,它同样展示了和前一个例子相同的问题:如果我点击了发送按钮,这个组件应该发送的是,我点击按钮那一刻输入的信息。

OK,我们现在知道,函数组件会默认捕获props和state。但是,如果你想读取最新的props、state呢,而不是某一时刻render时捕获的数据? 甚至我们想在 将来某个时刻读取旧的props、state 呢?

在类组件里,我们只需要简单的读取 this.props this.state 就能访问到最新的数据,因为react会修改this。在函数组件里,我们同样可以拥有一个可变数据,它可以在每次render里共享同一份数据。这就是hooks里的 useRef:

function MyComponent() { const ref = useRef(null); // You can read or write `ref.current`. // ...}

但是,你需要自己维护 ref 对应的值。

函数组件里的 ref 和类组件中的实例属性 扮演了相同的角色 。你或许已经熟悉 DOM refs,但是hooks里的 ref 更加通用。hooks里的 ref 只是一个容器,你可以往容器里放置任何你想放的东东。

甚至看起来,类组件里的 this.something 也和hooks里的 something.current 相似,他们确实代表了同一个概念。

默认情况下,react不会给函数组件里的props、state创造refs。大多数场景下,你也不需要这样做,这也需要额外的工作来给refs赋值。当然了,你可以手动的实现代码来跟踪最新的state:

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; };

如果我们在 showMessage里读取 message字段,那么我们会得到我们点击按钮时,输入框的值。但是,如果我们读取的是 latestMessage.current,我们会得到输入框里最新的值——甚至我们在点击发送按钮后,不断的输入新的内容。

你可以对比 这两个demo 来看看其中的不同。

通常来讲,你应该避免在render函数中,读取或修改 refs ,因为 refs 是可变的。我们希望能保证render的结果可预测。但是,如果我们想要获取某个props或者state的最新值,每次都手动更新refs的值显得很枯燥 。这种情况下,我们可以使用 useEffect 这个hook:

function MessageThread() { const [message, setMessage] = useState(''); // Keep track of the latest value. const latestMessage = useRef(''); useEffect(() => { latestMessage.current = message; }); const showMessage = () => { alert('You said: ' + latestMessage.current); };

(demo 在这里)

我们在 useEffect 里去更新 ref 的值,这保证只有在DOM更新之后,ref才会被更新。这确保我们对 ref 的修改,不会破坏react中的一些新特性,比如 时间切分和中断 ,这些特性都依赖 可被中断的render。

像上面这样使用 ref 不会太常见。大多数情况下,我们需要捕获props和state 。但是,在处理命令式API的情况下,使用 ref 会非常简便,比如设置定时器,订阅事件等。记住,你可以使用 ref 来跟踪任何值——一个prop,一个state,整个props,或者是某个函数。

使用 ref 在某些性能优化的场景下,同样适用。比如在使用 useCallback 时。但是,使用useReducer 在大多数场景下是一个 更好的解决方案 。

总结

在这篇文章里,我们回顾了类组件中常见的一个问题,以及怎样使用闭包来解决这个问题。但是,你可能已经经历过了,如果你尝试通过指定hooks的依赖项,来优化hooks的性能,那么你很可能会在hooks里,访问到旧的props或state。这是否意味着闭包会带来问题呢?我想不是的。

正如我们上面看到的,在一些不易察觉的场景下,闭包帮助我们解决掉这些微妙的问题。不仅如此,闭包也让我们在 并行模式 下更加容易写出没有bug的代码。因为闭包捕获了我们render函数运行时的props和state,使得并行模式成为可能。

到目前为止,我经历的所有情况下,访问到旧的props、state,通常是由于我们错误的认为"函数不会变",或者"props始终是一样的"。实时上不是这样的,我希望在本文里,能够帮助你了解到这一点。

在我们使用函数来开发大部分react组件时,需要更正我们对于 代码优化 和 哪些状态会改变 的认知。

正如 Fredrik说的:

在使用react hooks过程中,我学习到的最重要规则就是,"任何变量,都可以在任何时间被改变"

函数同样遵守这条规则。

react函数始终会捕获props、state,最后再强调一下。

译注: 有些地方不明白怎么翻译,有删减,建议阅读原文!https://overreacted.io/how-are-function-components-different-from-classes/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值