前端面试宝典React篇04 类组件与函数组件有什么区别呢?

发布于:2022-12-22 ⋅ 阅读:(595) ⋅ 点赞:(0)

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

破题

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

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

  • 在确认共性的基础上,才能找到它独特的个性;

  • 再通过具体的场景逐个阐述它的个性。

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

  • 你对组件的两种编写模式是否了解;

  • 你是否具备在合适的场景下选用合适技术栈的能力。

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

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

承题

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

  • 从组件的使用方式和表达效果来总结相同点;

  • 从代码实现、独有特性、具体场景等细分领域描述不同点。

Lark20201208-185659.png

但是用这样的方式去描述类组件与函数组件的不同点似乎有些混乱,我们可以列出很多的重点,以至于似乎没有了重点,所以我们还需要再思考。如此多的不同点,本质上的原因是什么?为什么会设计两种不同的方式来完成同一件事,就像函数设计中为什么有 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 = () => {
     // 这里每次都是取最新的 this.props。
    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 的未来发展。

Lark20201208-185707.png

总结

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

《大前端高薪训练营》


精选评论

**辉:

我太菜了吧,还是没听懂改变小白小黄的例子,按这样岂不是说类组件更好,实时更新

**举:

类组件未来会用的更少对吧?hook 理论上可以让整个应用都使用函数式组件。

    讲师回复:

    是的,React 团队更倾向于推广 Hooks 来取代现有的模式,但 Hooks 这种模式能走多远还得看社区的使用与反馈情况。如果看过 React Future(https://github.com/reactjs/react-future)就会发现从现在去预估未来真的很难、很难预测未来 React 团队会不会又突然调整方向,在那之后当下的规划与流行在未来还会占多少。但从目前社区的情况而言,我感觉 Hooks 和类组件各占一半,暂时还难分伯仲。

console_man:

老师讲得清清楚楚,明明白白的。还有代码辅助。思路非常的清晰

    编辑回复:

    感谢反馈,加油,学完一定会有所收获的。

s(t):

回复**辉,我在过去的某个节点想要获取user的信息就该获取过去的user,而不是最新的user。对应着ui=f(data),只要输入不变,输出就是固定可预测的。过去的data对应过去的渲染结果。

**龙:

“那为什么会这样呢?因为当切换下拉框后,新的 user 作为 props 传入了类组件中,所以此时组件内的 user 已经发生变化了” 请问老师这里为什么函数组件没有接收到父组件传来的props啊?

    讲师回复:

    接收了,但函数 showMessage 和 handleClick 被调用时使用的是前值的引用。

**明:

Hooks 更易于 tree-shaking 是吗?

    讲师回复:

    tree shaking 只依赖于 ES2015 module 的结构,这个并不依赖于某种技术手段,完全依赖于代码的语法结构。
要说 hooks 更易于 tree shaking,我觉得缺乏依据。当然,如果有更好的案例说明,可以继续留言,感谢分享。
tree shaking 可以看一下 https://webpack.js.org/guides/tree-shaking/

**4829:

何为心智模型?

    讲师回复:

    引用唐纳德·诺曼的话来讲,就是“心智模型是存在于用户头脑中对一个产品应具有的概念和行为的知识。这种知识可能来源于用户以前使用类似产品沉淀下来的经验,或者是用户根据使用该产品要达到的目标而对产品概念和行为的一种期望。”
一个具体的例子而言,当我们习惯了左侧行驶方向后 ,突然去右侧行驶方向的国家,那么就需要重新建立心智模型。不光是行驶方向的区别,你的座位也变化到了右边,操作习惯,视野方向,都需要调整适应。
不知道这样的解释是否清洗,如果还有问题,欢迎继续留言。

*瑞:

对于本文的取props的例子。函数组件取到捕获值才是 React 的正确行为的吗?

    讲师回复:

    不能叫正确行为,应该是捕获的执行方式理解成本更低,用起来负担更轻。

**梁:

hooks出来后,慢慢的减少使用class组件了都

*康:

这个面试必考

    编辑回复:

    是的,老师讲的都是面试常考的

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到