[译]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 (<button onClick={handleClick}>Follow</button>);
}

组件会渲染一个按钮,当点击按钮的时候,模拟了一个异步请求,并且在请求的回调函数里,显示一个弹窗。比如,如果 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 <button onClick={this.handleClick}>Follow</button>;}
}

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

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

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

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

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

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

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

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

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

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

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

在这个例子里,使用函数组件的实现是正确的,类组件的实现明显有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相关联的 propsstate

然而,我们在按钮点击事件处理函数里,使用定时器(setTimeout)延迟调用 showMessage,打破了showMessagethis.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 <button onClick={this.handleClick}>Follow</button>;}
}

这种方式 可以解决这个问题 。然而,这个解决方式让我们引入了冗余的代码,随着时间推移,容易引入别的问题。如果我们的 showMessage方法要读取更多的props呢?如果showMessage还要访问state呢?如果 showMessage 调用了其他的方法,而那个方法读取了别的状态,比如this.props.somethingthis.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 <button onClick={this.handleClick}>Follow</button>;}
}

很遗憾,上面的代码 不能 解决这个问题!记住,导致这个问题的原因,是我们读取 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 <button onClick={handleClick}>Follow</button>;}
}

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

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

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

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

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

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

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

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

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

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

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

可以看到,功能完全是正确的。(再次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 (<><input value={message} onChange={handleMessageChange} /><button onClick={handleSendClick}>Send</button></>);
}

(在线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/

onclick=两个函数_[译]React函数组件和类组件的差异相关推荐

  1. React函数组件和类组件的区别

    定义组件有两个要求: 组件名称必须以大写字母开头 组件的返回值只能有一个根元素 函数组件 函数组件接收一个单一的 props 对象并返回了一个React元素 类组件 class Welcome ext ...

  2. react render相关 【类组件、函数组件 】

    [类组件] 大概就是如果是class的写法,并且extends React.component 就要手动写render [函数组件] 如果是const App = ()=> {} 那么就酸函数组 ...

  3. react基础入门,类组件和函数组件,state,props,refs

    React入门 目录 React入门 React入门 Vue跟React的异同点 相同点 不同点 Vue小建议 1. 不需要响应式的数据应该怎么处理? 2. Key 3. 数据结构 React 教程 ...

  4. string.h包含哪些函数_多个函数组合拳专治不规则时间转化难题|Excel134

    小伙伴们好,今天分享一个关于不规则时间转换的小技巧. 这是Excel学员群里的学员提出的,我觉得这个案例很典型,所以分享给大家,希望能够帮助在此方面有同样困惑的小伙伴. 问题描述:计算A列学习时长的小 ...

  5. ltrim函数_常用基础函数

    今天分享的是一些常用的函数,类似于Python一样,SQL也支持用函数来处理数据,合理地使用会在日常工作中带了非常多地遍历. 去除空格函数:trim(), ltrim(), rtrim() selec ...

  6. C++_虚继承_虚函数_纯虚函数(多继承的二义性,多态)

    基本信息 每一个类都有一个虚表,以及虚表指针; 虚表的内容是编译器决定的,虚表中用于存放虚函数的指针, 程序运行时的类型信息等; 每个多态对象都存放着一个指向当前类型的虚表的指针, 该指针在构造函数中 ...

  7. ege限制鼠标移动的函数_浅谈函数节流和函数防抖

    什么是函数节流和函数防抖?下面本篇文章就来给大家浅谈一下函数节流和函数防抖.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 前言 事件的触发权很多时候都属于用户,有些情况下会产生问题 ...

  8. correl函数_【Excel函数】CORREL函数 - 曹海峰个人博客

    本文介绍Microsoft Excel中CORREL函数的语法和用法. CORREL函数适用于:Excel2003.Excel2007.Excel2010.Excel2013.Excel2016. 函 ...

  9. 对勾函数_对勾函数的图像及其性质1.pptx

    对勾函数的图像及其性质1.pptx 热烈欢迎各位老师前来听课! 况秀玉春 1.给出一个确定的函数常从几个方面研究它: 定义域.值域.奇偶性. 单调性.函数图象 ⑴.函数的定义域 函数 y=f(x) 中 ...

最新文章

  1. apache php 调优_记一次apache+php调优
  2. python编程实例详解-Python编程之列表操作实例详解【创建、使用、更新、删除】...
  3. 基于DPI(深度报文解析)的应用识别
  4. Linux服务器的最大内存和CPU数
  5. 复数混频发射机原理与仿真
  6. 你一定听过这些不太标准的技术圈发音...
  7. api接口返回动态的json格式?我太难了,尝试一下 linq to json
  8. TCP的定时器系列 — 零窗口探测定时器(有图有代码有真相!!!)
  9. 廖雪峰JS教程学习记录----Map和Set
  10. Hive 大数据表性能调优
  11. 体验ASP.NET 2.0客户端回调功能(CallBack)
  12. 【一天一个C++小知识】013.std:map-不存在的key查找其value
  13. HTML5离线缓存(Application Cache)
  14. PayPal个人账户不能提现了吗?怎么解决?
  15. 聚美优品广告词和经典分析
  16. 微信小程序成语小秀才,成语接龙超详细搭建教程
  17. 如何用Excel制作工作计划,跟踪任务进度,快来学习吧
  18. AppStore上线审核
  19. Anaconda安装老版本tensorflow
  20. ISO三体系认证多少钱?

热门文章

  1. 用户描述 和 组描述 和 远程连接
  2. 第三章 处理机调度与死锁
  3. java语言基础及集合基础大总结
  4. struts2的OGNL表达式理解(一)
  5. 浅谈Java反射(Reflect)技术--常用方法
  6. 网络编程3之TCP/IP协议
  7. ACM中Java输入输出
  8. 交互式数据包处理程序 Scapy 入门指南
  9. Spring Cloud(四) API网关Zuul
  10. excel报表服务器作用,Excel各种图表的应用范围及用途介绍