React 类组件和函数组件的理解

191 阅读9分钟

一、React Hook出现的背景

1.如何理解【有状态组件】和【无状态组件】

我们知道react最开始对class和函数组件的定位分别是有状态组件和无状态组件。具体表现在继承class类创建的组件和纯函数组件。

所谓的状态就是组件内部维护数据的state。因此有状态组件就是组件内有state,无状态组件就是组件内没有state。
无状态组件:无状态组件(Stateless Component)是最基础的组件形式,由于没有状态的影响所以就是纯静态展示的作用。一般来说,各种UI库里也是最开始会开发的组件类别。如按钮、标签、输入框等。它的基本组成结构就是属性(props)加上一个渲染函数(render)。由于不涉及到状态的更新,所以这种组件的复用性也最强。
有状态组件:在无状态组件的基础上,如果组件内部包含状态(state)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件(Stateful Component)。有状态组件通常会带有生命周期(lifecycle),用以在不同的时刻触发状态的更新。这种组件也是通常在写业务逻辑中最经常使用到的,根据不同的业务场景组件的状态数量以及生命周期机制也不尽相同。

总结:在React中,我们通常通过props和state来处理两种类型的数据。
无状态组件(也称为哑组件)使用props来存储数据,props是只读的,只能由父组件设置。
有状态组件(也称为智能组件)使用state来存储数据。

2.为什么函数组件内部无法定义和维护state

首先维护状态不是函数中的维护变量,

import { sculptureList } from './data.js';

export default function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <h3>  
        {index + 1}
      </h3>
      <button onClick={handleClick}>
        Next
      </button>
    </>
  );
}

如下所示,在函数组件中定义了变量,也在click时候改变了变量。但是视图上并没有更新。而我们看下class组件是如何维护state的:
1.先将变量存储在state中(这一点函数组件可以做到);
2.更改变量是使用类组件提供的API:setState,这样才能保证数据的更新会同步到视图中(这个setState函数组件中没有提供)。因此函数组件无法实现:变量改变同步更新到视图。

class Counter1 extends React.Component {
  constructor() {
    this.state = {};
  }
  increment = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  render() {
    return (
      <div>
        <p>Counter 1: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

总结:
函数组件内部无法定义和维护state的原因是:函数组件中没有提供像class组件那样的可以讲数据更新同步到视图的API:setState。
函数组件是无状态组件的特点
1.表现在无法具有类组件维护state的能力
2.原因是没有提供类似setState那样的API,实现数据的更新同步到视图。

3.class类组件存在的问题

3.1 “组件”逻辑复用困难问题

首先声明一点,这里的要复用的逻辑是指什么?是不是utils中纯函数逻辑?
显然,utils中放的是纯函数逻辑,如果组件要复用就直接引用即可复用。不存在苦难的说法。所谓复用苦难,就是说不能复用,难以复用。所以这里的针对的复用逻辑是“组件逻辑”。

什么是“组件逻辑”?
组件逻辑是指class类组件才有的特定逻辑(生命周期逻辑 && 状态逻辑-对state定义和处理)。
1.【场景1】:对组件中生命周期复用。类组件中特有生命周期,函数组件中不存在生命周期,而我们往往在生命周期中会调用fetch请求,并处理一些数据逻辑。那么如果存在一个场景:组件A和组件B都要在生命周期中调用相同的fetch,请问怎么样才能复用? 显然,我们需要把对应的生命周期抽出来,提供复用。
2.【场景2】:对组件中state状态定义&处理逻辑复用。如下所示:我们定义了两个组件,两个组件中存在的公共逻辑是:
(1) 定义state状态并初始化count值为0;
(2) 绑定一个事件并在组件内定义一个事件函数 (3) 定义事件函数内填写处理逻辑:处理逻辑是-每次+1

class Counter1 extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  render() {
    return (
      <div>
        <p>Counter 1: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

class Counter2 extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  render() {
    return (
      <div>
        <p>Counter 2: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

此时,假设我们能做的只能是将事件函数定义内部的逻辑抽象。即,事件函数的处理逻辑是-每次+1,抽象为一个函数逻辑放在utils中。但是其他的公共部分:
1.定义state状态并初始化count值为0;(不能用utils抽象)
2.事件函数的定义(不能用utils抽象)
3.定义事件函数内填写处理逻辑:处理逻辑是-每次+1 (可以用utils抽象)
这里的1,2两点就属于组件逻辑,那么如果要做到最大程度的复用,就必须解决组件逻辑的复用。

3.2 this的不确定性

3.3 后期维护性困难

因为class不易拆分,很难拆分和重构。从设计模式来看,相比之下,函数式编程风格更适合前端代码开发。

4. “组件逻辑”复用的方案

如何评估一个问题对应的解决方案好坏(两个方面)

1.该方案是否彻底解决原有问题;
2.该方案本身是否带来新问题:
(1) 对原有体系的破坏性
(2) 方案自身问题 (上手难度,操作难易等)

(1) Minxin

Mixin评估
方案采用方【React】、【vue】
优点解决了组件逻辑复用问题
缺点1.对原有体系存在破坏性(变量覆盖,变量污染问题);2.当mixin数量过多时候,变量溯源困难

采用方:【React】、【vue】
优点:解决了组件逻辑复用问题
缺点:1.对原有体系存在破坏性(变量覆盖,变量污染问题); 2.当mixin数量过多时候,变量溯源困难

(2) 高阶组件

如何理解高阶组件

理解高阶组件文章:juejin.cn/post/740625…

问题:为什么vue不使用高阶组件?
vue本身也是可以使用高阶组件的,因为vue自身的DSL比react要重,如果要使用高阶组件的话会增加更多的复杂性和使用成本,因此vue中依然保持Mixin的复用方案。

高阶组件评估
方案采用方【React】
优点解决了组件逻辑复用问题
缺点

(3) Hooks模式

总结

mixin模式 image.png HOC模式 image.png Hooks模式:

image.png

mixin && 高阶组件方式 && hooks 对比
从使用方式的外观表现上来看,组件使用mixin和hooks是使用什么就引用什么。而高阶组件则是对组件进行重新包装(外面嵌套一层父组件,这样才能将公共复用逻辑放在父组件中,以props传给子组件使用)。因此,高阶组件使用模式是类似洋葱模型那样包裹型的。而不是像hooks那样轻。

方案1.提供新的class-API来解决
方案2.另起炉灶增强函数组件

为什么要通过增强函数组件(即提供hooks)来替代类组件来解决问题?
原因1:其实相比于面向对象编程,函数式编程更符合前端的编程习惯:输入数据,render渲染,输出视图。 那么既然选择方案2代替,那么就要先将函数组件功能增强到能完全平替class组件,然后再解决class的缺点。

历史功能差距:生命周期、状态管理 解决class问题:逻辑服用 综上所述:在函数式组件中,通过hooks完成生命周期、状态管理、逻辑复用等几乎全部class组件开发工作的能力。

受到react影响,为什么vue也选择了hooks? juejin.cn/post/706695… vue的hooks和react的有什么区别?

juejin.cn/post/708892…

二、Hooks代替class组件设计方案

2.1 抹平class功能差距

2.1.1 状态管理实现

通过使用useState,让函数组件可以管理视图数据

2.1.2 组件生命周期实现

组件的的生命周期主要分为四个大阶段:组件的创建、挂载、更新、卸载。 而我们日常开发中熟悉的各种生命周期钩子,也是围绕着这四个阶段来设置对应的钩子的。

(1) 类组件的生命周期

step1 - 挂载阶段(Mounting)
组件被实例化并插入 DOM 中的过程。这个阶段包括以下生命周期方法:

  • 1-1 constructor() - 作用:初始化state,例如:this.state = { }
  • 1-2 static getDerivedStateFromProps() 作用:获取props数据
  • 1-3 render()
  • 1-4 componentDidMount() - 作用:执行初始化操作(比如发起网络请求、订阅事件、获取初始数据)

step2 - 更新阶段(Updating)
组件的 props 或 state 发生变化,导致组件重新渲染的过程。这个阶段包括以下生命周期方法:

  • 2-1 static getDerivedStateFromProps() 作用:获取props数据同挂载阶段的钩子
  • 2-2 shouldComponentUpdate() - 作用:阻止子组件无效渲染,提升性能
  • 2-3 render() - 作用:返回JSX模版
  • 2-4 getSnapshotBeforeUpdate() - 作用:获取更新前的 DOM 快照或执行一些 DOM 操作,返回的值将作为componentDidUpdate方法的第三个参数传递。
  • 2-5 componentDidUpdate() - 作用:拿到更新后的DOM

step3 - 卸载阶段(Unmounting)
组件从 DOM 中移除的过程。这个阶段只包含一个生命周期方法:

  • componentWillUnmount() - 作用:取消订阅操作
(2) 函数组件中的hooks实现
挂载阶段(4个)Hooks 组件
constructoruseState
getDerivedStateFromPropsuseState里的update 函数
render函数本身的render
componentDidMounteduseEffect
更新阶段(5个)Hooks 组件
getDrivedStateFromPropsuseState里的update 函数
shouldComponentUpdateuseMemo
render函数本身的render
getSnapShotBeforeUpdate--
componentDidUpdateuseEffect
卸载阶段(1个)Hooks 组件
componentWillUnmountuseLayoutEffect

useEffect模拟生命周期
1.模拟componentDidMount: 组件首次挂载是欧

参考文献:
新旧生命周期对比:juejin.cn/post/728554…
对应到hooks:juejin.cn/post/684490…
举例:juejin.cn/post/703952…

lzwdot.com/docs/29414/

2.1.3 class的pure-component实现

2.2 解决class存在问题

(1) 解决组件逻辑复用

三、Hooks具体原理解析

参见链接: