你可能真的不需要写class组件了

导读

本文以一种循序渐进的方式提出了一种更干净、更易于维护地书写React函数组件的方式,适合有一定React使用基础的前端er。

背景

笔者最近接了一个新项目,在搭(拷)建(贝)项目框架的时候决定把所有依赖都升级到最新版,但是在升级到 React Router V6 的时候遇到了点阻力:原来的写法基本要推翻重来!比如,笔者之前经常这样导出一个模块,方便在任意地方进行跳转操作:

import { createBrowserHistory } from 'history';
const history = createBrowserHistory({
  basename: '/',
});
export default history;
复制代码

但是在V6下,这种方式就不支持了,甚至连 withRouter 这个高阶组件都没有提供!你想跳转,只能通过 Link 等组件或者相关hooks来实现。此时笔者心中升起了一个疑问:class组件的跳转怎么办? 使用 Link 这种组件的形式十分的不方便,那只能自己实现一个withRouter —— 虽然不难,但是也不禁让人思考,这是要放弃class组件的节奏吗?

正文

其实React Router已经不是个例了,很多其他库也是对 hooks 有更强的支持,比如 react-query。究其所以,是因为 hooks 这种方式能脱离class组件的 this 去做状态更新,更容易实现一些“骚”操作。

说到这里,我提个问题:

你们写一个组件的时候是怎么选择使用class组件还是函数组件呢?

要做好这个选择,那就要对比下各自的优缺点(以下为笔者使用习惯的下的对比,仅供参考):

class组件相比函数组件:

  1. 状态集中 所有状态都挂在 this.state 下。而比较复杂的函数组件可能有 n 个 useState(当然,如果你的组件能确保在一个地方使用或者允许状态共享,你可以使用更优秀的 useReducer 方案)
  2. 状态更新入口统一 仅能通过 this.setState 来主动完成状态更新,只要搜索 setState 就能找到哪些地方能导致页面状态的更新。而函数组件可能有 n 个 setXX 函数了, 你搜都不好搜。尤其是在看别人写的大组件的时候真是头疼!
  3. 不变的callback 你可以将定义在class组件上的一个函数转递给组件,而不用担心这个函数会发生变化,有助于性能提升。而函数组件你可能就需要使用 useCallback 了。少了还行,多了还是让人头疼,而且还要注意闭包问题。

函数组件相对于class组件:

  1. 写法简单 在写小组件的时候,使用函数组件非常爽。
  2. 方便的变化探测 函数组件监视props的变化非常方便,搞个 useEffect 依赖写上监视对象就行了。这一点甩class组件几条gai
  3. 眼花缭乱的回调函数 注意,这个是劣势。在有些复杂的组件里面,你会发现,组件体内有好多回调函数的声明,先不说每次渲染这个回调函数都会新建这个性能问题(其实大多数情下确实不必考虑),这些函数会把组件体撑的十分臃肿,找个东西都困难。维护起来也是千行泪啊

所以,基于以上,以前笔者的习惯是:状态复杂的组件使用class组件,简单的使用函数组件!但是这种选择方式也还是有bug,因为前期的简单组件还是可能慢慢变为复杂组件!

尤其,现在 React Router V6 一出,这个选择又头疼了!

我们能不能不做选择,而是以一种集中了各自优势的方式写呢?!

融合

首先,写组件要么使用class方式,要么使用函数方式,你跳不出这个圈子。但是写函数组件在某些地方确实方便,所以如果我们把class组件的优点能搬到函数组件亦或者说把函数组件的短板补足,那毫无疑问,以后就只写函数组件了,升级React Router V6也不头疼了。

状态集中
状态更新入口统一

这俩优点其实很好搬到函数组件,我们只需要使用一个对象类型的状态就行了。类似如下:

const [state, setInnerState] = useState({ prop1: '', prop2: 0 });
const setState = (newState) => {
  setInnerState({ ...state, newState });
};
复制代码

后续只需要使用setState就能设置任意状态值。其实最亟待解决还要数 眼花缭乱的回调函数 这个问题;这个问题只要解决了 不变的callback 这个优点也能应该随之而来。

那如何解决 眼花缭乱的回调函数 这个问题呢?

要解决这个问题,那就要把函数组件体内的那些callback移出组件体,但是移出去之后会出现新的问题,因为对于某个callback,它可能需要:

  1. 读取当前组件状态。
  2. 读取当前props。
  3. 更新组件状态。
  4. 知道组件是否还活着(某些计时器可能用到)。

各位,想想这些问题怎么解决。要知道,这些callback一旦定义在组件体外,它已经失去了对 state props 的闭包,通过常规手段已经拿不到这些数据了。

可能有些同学会想到发布订阅、或者观察者模式等,理论上也能实现,比如你要拿到当前state,你发个通知,组件接收到通知回给你当前state,但是这种方式实现起来不够直接了当,有点麻烦。

我们需要一种更干净、直接了当的在运行时和组件经行数据交换的方式!

钥匙

还真有一种方式: Generator

想到这种方式,是受到 redux-saga(上手比较陡峭,不熟悉的可以去了解下) 启发,感谢 redux-saga.

可能有些同学不熟悉 Generator,那我们首先做个例子吧(基础概念就不说了,需要的自取

function* fn1(){
    const state = yield "我要最新状态";
    console.log(state);
}
复制代码

以上,声明了一个 Generator Function,下面我们来执行它

const ge = fn1();
let geRes = ge.next();
复制代码

好,下面关键点来了,我们要拿到 yield 的数据,并做一些处理

if (geRes.value === '我要最新状态') {
  geRes = ge.next({ state1: 0 });
}
复制代码

注意上面 next方法的调用, 有个传值的操作,这个是Generator Function的特性,正式这个特性,让我们能有来有回地在运行时进行数据的交互。

这个执行过后,控制台就能打印出 { state1: 0 } 这个值了。

以上就是一个 Generator Function 的非常贴合我们需求的一种用法的例子。

下面我来总结下它的突出特点:

  1. 执行过程中能向外不止一次输出值(多个yield)。
  2. 执行过程中外界能向里面不止一次输入值(多个next(value) 配合多个yield)。

大招

所以,如果我们在函数组件体内,能有一个 Generator Function 的执行者,这个执行者能访问最新的 state props,那么是不是我们在函数组件体外定义的Generator Function就也能拿到最新的 state props了!而且不光是能拿到最新数据,我们还能根据yield出的数据的不同,通过执行者做一些其他操作,比如更新组件状态。其实就基本上和在组件体内定义的函数达到相同的效果了。

说到这里,满足我们需求的方案基本上已经出来了,我再简单描述下:

  1. 在函数组件体外定义 Generator Function (其实如果不牵涉读取state 以及props,也可以定义成普通函数)
  2. 将第一步声明的函数在函数组件体内转换成受控制的函数(执行者)
  3. 组件内使用受控制的函数,不直接使用原函数

只要在第二步保证只转变一次,使结果不变(useMemo),那么 不变的callback 这个优点也就有了。

各位已经可以按照上面的思路来实现一个自己的方案了!

广告

笔者已经照此思路实现了一版,并已经在新项目中使用、上线。新项目中,全部都是函数组件,组件本身不管复杂与否都非常简洁明了!

版本特点:

  1. 体外函数支持普通函数、异步函数、GeneratorFunction、AsyncGeneratorFunction
  2. 支持yield一个Promise,并等待promise执行完毕
  3. 完整的TS支持
  4. 充分的测试用例

如果各位觉得自己已清楚熟悉方案原理,不想浪费时间去实现一版,可以直接使用笔者的实现版本,或者参考笔者的实现。

GitHub地址:

github.com/elvinzhu/rf… (欢迎star,助笔者实现千星梦 ^_^)

npm包:

npm install rfc-state --save
复制代码

最后

感谢阅读,如有错误之处或者问题需要探讨,评论区见。

Guess you like

Origin juejin.im/post/7060840672449789988