前端面试宝典之 React 篇 - 某一线大厂资深前端工程师 - 拉勾教育

前面讲了 “是什么”“为什么”“如何避免” 这三种类型的问题,本讲我们通过分析 “类组件与函数组件有什么区别呢?” 这个问题,来看看 “有什么区别” 这类型的问题该怎么回答。

破题

正如前面的几讲内容所说,答题不仅是告知答案,更是要有表达上的完整性,使用表达的技巧去丰富面试表现。以这样的思路,我们再来分析下 “有什么区别” 这类题应该如何应对。

描述区别,就是求同存异的过程

  • 在确认共性的基础上,才能找到它独特的个性;
  • 再通过具体的场景逐个阐述它的个性。

针对 “类组件与函数组件有什么区别呢?” 这一面试题,面试官需要知道:

  • 你对组件的两种编写模式是否了解;
  • 你是否具备在合适的场景下选用合适技术栈的能力。

类组件与函数组件的共同点,就是它们的实际用途是一样的,无论是高阶组件,还是异步加载,都可以用它们作为基础组件展示 UI。也就是作为组件本身的所有基础功能都是一致的。

那不同点呢?我们可以尝试从使用场景、独有的功能、设计模式及未来趋势等不同的角度进行挖掘。

承题

基于以上的分析,我们可以整理出如下的答题思路:

  • 从组件的使用方式和表达效果来总结相同点;
  • 从代码实现、独有特性、具体场景等细分领域描述不同点。

04 | 类组件与函数组件有什么区别呢? - 图1

但是用这样的方式去描述类组件与函数组件的不同点似乎有些混乱,我们可以列出很多的重点,以至于似乎没有了重点,所以我们还需要再思考。如此多的不同点,本质上的原因是什么?为什么会设计两种不同的方式来完成同一件事,就像函数设计中为什么有 callback 与链式调用两种模式?就需要你去找差异点中的共性作为主线

入手

相同点

组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。

你甚至可以将一个类组件改写成函数组件,或者把函数组件改写成一个类组件(虽然并不推荐这种重构行为)。从使用者的角度而言,很难从使用体验上区分两者,而且在现代浏览器中,闭包和类的性能只在极端场景下才会有明显的差别。

所以我们基本可认为两者作为组件是完全一致的。

不同点

基础认知

类组件与函数组件本质上代表了两种不同的设计思想与心智模式。

  • 类组件的根基是 OOP(面向对象编程),所以它有继承、有属性、有内部状态的管理。
  • 函数组件的根基是 FP,也就是函数式编程。它属于 “结构化编程” 的一种,与数学函数思想类似。也就是假定输入与输出存在某种特定的映射关系,那么输入一定的情况下,输出必然是确定的。

相较于类组件,函数组件更纯粹、简单、易测试。 这是它们本质上最大的不同。

有一个广为流传的经典案例,是这样来描述函数组件的确定性的(有的文章会将这种确定性翻译为函数组件的值捕获特性),案例中的代码是这样的:

  1. const Profile = (props) => {
  2. const showMessage = () => {
  3. alert('用户是' + props.user);
  4. };
  5. const handleClick = () => {
  6. setTimeout(showMessage, 3 * 1000);
  7. };
  8. return (
  9. <button onClick={handleClick}>查询</button>
  10. );
  11. }

请注意,由于这里并没有查询接口,所以通过 setTimeout 来模拟网络请求。

那如果通过类组件来描写,我们大致上会这样重构,代码如下:

  1. class Profile extends React.Component {
  2. showMessage = () => {
  3. alert('用户是' + this.props.user);
  4. };
  5. handleClick = () => {
  6. setTimeout(this.showMessage, 3 * 1000);
  7. };
  8. render() {
  9. return <button onClick={this.handleClick}>查询</button>;
  10. }
  11. }

表面上看这两者是等效的。正因为存在这样的迷惑性,所以这也是此案例会如此经典的原因。

接下来就非常神奇了,也是这个案例的经典步骤:

  • 点击其中某一个查询按钮;
  • 在 3 秒内切换选中的任务;
  • 查看弹框的文本。
  1. import React from "react";
  2. import ReactDOM from "react-dom";
  3. import ProfileFunction from './ProfileFunction';
  4. import ProfileClass from './ProfileClass';
  5. class App extends React.Component {
  6. state = {
  7. user: '小明',
  8. };
  9. render() {
  10. return (
  11. <>
  12. <label>
  13. <b> : </b>
  14. <select
  15. value={this.state.user}
  16. onChange={e => this.setState({ user: e.target.value })}
  17. >
  18. <option value="小明">Dan</option>
  19. <option value="小白">Sophie</option>
  20. <option value="小黄">Sunil</option>
  21. </select>
  22. </label>
  23. <h1>{this.state.user}</h1>
  24. <p>
  25. <ProfileFunction user={this.state.user} />
  26. <b> (function)</b>
  27. </p>
  28. <p>
  29. <ProfileClass user={this.state.user} />
  30. <b> (class)</b>
  31. </p>
  32. </>
  33. )
  34. }
  35. }
  36. const rootElement = document.getElementById("root");
  37. ReactDOM.render(<App />, rootElement);

这时,你将会看到一个现象:

  • 使用函数组件时,当前账号是小白,点击查询按钮,然后立马将当前账号切换到小黄,但弹框显示的内容依然还是小白;
  • 而当使用类组件时,同样的操作下,弹框显示的是小黄。

那为什么会这样呢?因为当切换下拉框后,新的 user 作为 props 传入了类组件中,所以此时组件内的 user 已经发生变化了。如下代码所示:

  1. showMessage = () => {
  2. alert('用户是' + this.props.user);
  3. };

这里的 this 存在一定的模糊性,容易引起错误使用。如果希望组件能正确运行,那么我们可以这样修改:

  1. showMessage = (user) => {
  2. alert('用户是' + user);
  3. };
  4. handleClick = () => {
  5. const {user} = this.props;
  6. setTimeout(() => this.showMessage(user), 3 * 1000);
  7. }

但在函数组件的闭包中,这就不是问题,它捕获的值永远是确定且安全的。有了这样一个基础认知,我们就可以继续探讨差异了。

独有能力

类组件通过生命周期包装业务逻辑,这是类组件所特有的。我们常常会看到这样的代码:

  1. class A extends React.Component {
  2. componentDidMount() {
  3. fetchPosts().then(posts => {
  4. this.setState({ posts });
  5. }
  6. }
  7. render() {
  8. return ...
  9. }
  10. }

在还没有 Hooks 的时代,函数组件的能力是相对较弱的。在那个时候常常用高阶组件包裹函数组件模拟生命周期。当时流行的解决方案是 Recompose。如下代码所示:

  1. const PostsList = ({ posts }) => (
  2. <ul>{posts.map(p => <li>{p.title}</li>)}</ul>
  3. )
  4. const PostsListWithData = lifecycle({
  5. componentDidMount() {
  6. fetchPosts().then(posts => {
  7. this.setState({ posts });
  8. })
  9. }
  10. })(PostsList);

这一解决方案在一定程度上增强了函数组件的能力,但它并没有解决业务逻辑掺杂在生命周期中的问题。Recompose 后来加入了 React 团队,参与了 Hooks 标准的制定,并基于 Hooks 创建了一个完全耳目一新的方案。

这个方案从一个全新的角度解决问题:不是让函数组件去模仿类组件的功能,而是提供新的开发模式让组件渲染与业务逻辑更分离。设计出如下代码:

  1. import React, { useState, useEffect } from 'react';
  2. function App() {
  3. const [data, setData] = useState({ posts: [] });
  4. useEffect(() => {
  5. (async () => {
  6. const result = await fetchPosts();
  7. setData(result.data);
  8. }()
  9. }, []);
  10. return (
  11. <ul>{data.posts.map(p => <li>{p.title}</li>)}</ul>
  12. );
  13. }
  14. export default App;

使用场景

从上一部分内容的学习中,可以总结出:

  • 在不使用 Recompose 或者 Hooks 的情况下,如果需要使用生命周期,那么就用类组件,限定的场景是非常固定的;
  • 但在 recompose 或 Hooks 的加持下,这样的边界就模糊化了,类组件与函数组件的能力边界是完全相同的,都可以使用类似生命周期等能力。

设计模式

在设计模式上,因为类本身的原因,类组件是可以实现继承的,而函数组件缺少继承的能力。

当然在 React 中也是不推荐继承已有的组件的,因为继承的灵活性更差,细节屏蔽过多,所以有这样一个铁律,组合优于继承。 详细的设计模式的内容会在 05 讲具体讲解。

性能优化

那么类组件和函数组件都是怎样来进行性能优化的呢?这里需要联动一下上一讲的知识了。

  • 类组件的优化主要依靠 shouldComponentUpdate 函数去阻断渲染。
  • 而函数组件一般靠 React.memo 来优化。React.memo 并不是去阻断渲染,它具体是什么作用还记得吗?赶紧翻翻上一讲的内容再次学习下吧。

未来趋势

由于 React Hooks 的推出,函数组件成了社区未来主推的方案。

React 团队从 Facebook 的实际业务出发,通过探索时间切片与并发模式,以及考虑性能的进一步优化与组件间更合理的代码拆分结构后,认为类组件的模式并不能很好地适应未来的趋势。 他们给出了 3 个原因:

  • this 的模糊性;
  • 业务逻辑散落在生命周期中;
  • React 的组件代码缺乏标准的拆分方式。

而使用 Hooks 的函数组件可以提供比原先更细粒度的逻辑组织与复用,且能更好地适用于时间切片与并发模式。

答题

相信通过以上的分析,当被问到这道面试题时,你已经知道如何应答了吧。

作为组件而言,类组件与函数组件在使用与呈现上没有任何不同,性能上在现代浏览器中也不会有明显差异。

它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。

之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。

但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。

其次继承并不是组件最佳的设计模式,官方更推崇 “组合优于继承” 的设计概念,所以类组件在这方面的优势也在淡出。

性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。

从上手程度而言,类组件更容易上手,从未来趋势上看,由于 React Hooks 的推出,函数组件成了社区未来主推的方案。

类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。

04 | 类组件与函数组件有什么区别呢? - 图2

总结

经过本讲的学习,你可以掌握类组件与函数组件的区别,对于组件的方方面面都有了大概的认识。那么组件是通过什么模式来设计的呢?我将会在下一讲与你详细探讨。

04 | 类组件与函数组件有什么区别呢? - 图3

《大前端高薪训练营》

对标阿里 P7 技术需求 + 每月大厂内推,6 个月助你斩获名企高薪 Offer。点击链接,快来领取!