前端面试宝典之 React 篇 - 某一线大厂资深前端工程师 - 拉勾教育
前面讲了 “是什么”“为什么”“如何避免” 这三种类型的问题,本讲我们通过分析 “类组件与函数组件有什么区别呢?” 这个问题,来看看 “有什么区别” 这类型的问题该怎么回答。
破题
正如前面的几讲内容所说,答题不仅是告知答案,更是要有表达上的完整性,使用表达的技巧去丰富面试表现。以这样的思路,我们再来分析下 “有什么区别” 这类题应该如何应对。
描述区别,就是求同存异的过程:
- 在确认共性的基础上,才能找到它独特的个性;
- 再通过具体的场景逐个阐述它的个性。
针对 “类组件与函数组件有什么区别呢?” 这一面试题,面试官需要知道:
- 你对组件的两种编写模式是否了解;
- 你是否具备在合适的场景下选用合适技术栈的能力。
类组件与函数组件的共同点,就是它们的实际用途是一样的,无论是高阶组件,还是异步加载,都可以用它们作为基础组件展示 UI。也就是作为组件本身的所有基础功能都是一致的。
那不同点呢?我们可以尝试从使用场景、独有的功能、设计模式及未来趋势等不同的角度进行挖掘。
承题
基于以上的分析,我们可以整理出如下的答题思路:
- 从组件的使用方式和表达效果来总结相同点;
- 从代码实现、独有特性、具体场景等细分领域描述不同点。
但是用这样的方式去描述类组件与函数组件的不同点似乎有些混乱,我们可以列出很多的重点,以至于似乎没有了重点,所以我们还需要再思考。如此多的不同点,本质上的原因是什么?为什么会设计两种不同的方式来完成同一件事,就像函数设计中为什么有 callback 与链式调用两种模式?就需要你去找差异点中的共性作为主线。
入手
相同点
组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。
你甚至可以将一个类组件改写成函数组件,或者把函数组件改写成一个类组件(虽然并不推荐这种重构行为)。从使用者的角度而言,很难从使用体验上区分两者,而且在现代浏览器中,闭包和类的性能只在极端场景下才会有明显的差别。
所以我们基本可认为两者作为组件是完全一致的。
不同点
基础认知
类组件与函数组件本质上代表了两种不同的设计思想与心智模式。
- 类组件的根基是 OOP(面向对象编程),所以它有继承、有属性、有内部状态的管理。
- 函数组件的根基是 FP,也就是函数式编程。它属于 “结构化编程” 的一种,与数学函数思想类似。也就是假定输入与输出存在某种特定的映射关系,那么输入一定的情况下,输出必然是确定的。
相较于类组件,函数组件更纯粹、简单、易测试。 这是它们本质上最大的不同。
有一个广为流传的经典案例,是这样来描述函数组件的确定性的(有的文章会将这种确定性翻译为函数组件的值捕获特性),案例中的代码是这样的:
const Profile = (props) => {
const showMessage = () => {
alert('用户是' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3 * 1000);
};
return (
<button onClick={handleClick}>查询</button>
);
}
请注意,由于这里并没有查询接口,所以通过 setTimeout 来模拟网络请求。
那如果通过类组件来描写,我们大致上会这样重构,代码如下:
class Profile extends React.Component {
showMessage = () => {
alert('用户是' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3 * 1000);
};
render() {
return <button onClick={this.handleClick}>查询</button>;
}
}
表面上看这两者是等效的。正因为存在这样的迷惑性,所以这也是此案例会如此经典的原因。
接下来就非常神奇了,也是这个案例的经典步骤:
- 点击其中某一个查询按钮;
- 在 3 秒内切换选中的任务;
- 查看弹框的文本。
import React from "react";
import ReactDOM from "react-dom";
import ProfileFunction from './ProfileFunction';
import ProfileClass from './ProfileClass';
class App extends React.Component {
state = {
user: '小明',
};
render() {
return (
<>
<label>
<b> : </b>
<select
value={this.state.user}
onChange={e => this.setState({ user: e.target.value })}
>
<option value="小明">Dan</option>
<option value="小白">Sophie</option>
<option value="小黄">Sunil</option>
</select>
</label>
<h1>{this.state.user}</h1>
<p>
<ProfileFunction user={this.state.user} />
<b> (function)</b>
</p>
<p>
<ProfileClass user={this.state.user} />
<b> (class)</b>
</p>
</>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
这时,你将会看到一个现象:
- 使用函数组件时,当前账号是小白,点击查询按钮,然后立马将当前账号切换到小黄,但弹框显示的内容依然还是小白;
- 而当使用类组件时,同样的操作下,弹框显示的是小黄。
那为什么会这样呢?因为当切换下拉框后,新的 user 作为 props 传入了类组件中,所以此时组件内的 user 已经发生变化了。如下代码所示:
showMessage = () => {
alert('用户是' + this.props.user);
};
这里的 this 存在一定的模糊性,容易引起错误使用。如果希望组件能正确运行,那么我们可以这样修改:
showMessage = (user) => {
alert('用户是' + user);
};
handleClick = () => {
const {user} = this.props;
setTimeout(() => this.showMessage(user), 3 * 1000);
}
但在函数组件的闭包中,这就不是问题,它捕获的值永远是确定且安全的。有了这样一个基础认知,我们就可以继续探讨差异了。
独有能力
类组件通过生命周期包装业务逻辑,这是类组件所特有的。我们常常会看到这样的代码:
class A extends React.Component {
componentDidMount() {
fetchPosts().then(posts => {
this.setState({ posts });
}
}
render() {
return ...
}
}
在还没有 Hooks 的时代,函数组件的能力是相对较弱的。在那个时候常常用高阶组件包裹函数组件模拟生命周期。当时流行的解决方案是 Recompose。如下代码所示:
const PostsList = ({ posts }) => (
<ul>{posts.map(p => <li>{p.title}</li>)}</ul>
)
const PostsListWithData = lifecycle({
componentDidMount() {
fetchPosts().then(posts => {
this.setState({ posts });
})
}
})(PostsList);
这一解决方案在一定程度上增强了函数组件的能力,但它并没有解决业务逻辑掺杂在生命周期中的问题。Recompose 后来加入了 React 团队,参与了 Hooks 标准的制定,并基于 Hooks 创建了一个完全耳目一新的方案。
这个方案从一个全新的角度解决问题:不是让函数组件去模仿类组件的功能,而是提供新的开发模式让组件渲染与业务逻辑更分离。设计出如下代码:
import React, { useState, useEffect } from 'react';
function App() {
const [data, setData] = useState({ posts: [] });
useEffect(() => {
(async () => {
const result = await fetchPosts();
setData(result.data);
}()
}, []);
return (
<ul>{data.posts.map(p => <li>{p.title}</li>)}</ul>
);
}
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 的未来发展。
总结
经过本讲的学习,你可以掌握类组件与函数组件的区别,对于组件的方方面面都有了大概的认识。那么组件是通过什么模式来设计的呢?我将会在下一讲与你详细探讨。
对标阿里 P7 技术需求 + 每月大厂内推,6 个月助你斩获名企高薪 Offer。点击链接,快来领取!