导读
本文以一种循序渐进的方式提出了一种更干净、更易于维护地书写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组件相比函数组件:
- 状态集中 所有状态都挂在
this.state
下。而比较复杂的函数组件可能有 n 个useState
(当然,如果你的组件能确保在一个地方使用或者允许状态共享,你可以使用更优秀的useReducer
方案) - 状态更新入口统一 仅能通过
this.setState
来主动完成状态更新,只要搜索 setState 就能找到哪些地方能导致页面状态的更新。而函数组件可能有 n 个setXX
函数了, 你搜都不好搜。尤其是在看别人写的大组件的时候真是头疼! - 不变的callback 你可以将定义在class组件上的一个函数转递给组件,而不用担心这个函数会发生变化,有助于性能提升。而函数组件你可能就需要使用
useCallback
了。少了还行,多了还是让人头疼,而且还要注意闭包问题。
函数组件相对于class组件:
- 写法简单 在写小组件的时候,使用函数组件非常爽。
- 方便的变化探测 函数组件监视props的变化非常方便,搞个
useEffect
依赖写上监视对象就行了。这一点甩class组件几条gai - 眼花缭乱的回调函数 注意,这个是劣势。在有些复杂的组件里面,你会发现,组件体内有好多回调函数的声明,先不说每次渲染这个回调函数都会新建这个性能问题(其实大多数情下确实不必考虑),这些函数会把组件体撑的十分臃肿,找个东西都困难。维护起来也是千行泪啊
所以,基于以上,以前笔者的习惯是:状态复杂的组件使用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,它可能需要:
- 读取当前组件状态。
- 读取当前props。
- 更新组件状态。
- 知道组件是否还活着(某些计时器可能用到)。
各位,想想这些问题怎么解决。要知道,这些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 的非常贴合我们需求的一种用法的例子。
下面我来总结下它的突出特点:
- 执行过程中能向外不止一次输出值(多个yield)。
- 执行过程中外界能向里面不止一次输入值(多个next(value) 配合多个yield)。
大招
所以,如果我们在函数组件体内,能有一个 Generator Function 的执行者,这个执行者能访问最新的 state
props
,那么是不是我们在函数组件体外定义的Generator Function就也能拿到最新的 state
props
了!而且不光是能拿到最新数据,我们还能根据yield出的数据的不同,通过执行者
做一些其他操作,比如更新组件状态。其实就基本上和在组件体内定义的函数达到相同的效果了。
说到这里,满足我们需求的方案基本上已经出来了,我再简单描述下:
- 在函数组件体外定义 Generator Function (其实如果不牵涉读取state 以及props,也可以定义成普通函数)
- 将第一步声明的函数在函数组件体内转换成受控制的函数(执行者)
- 组件内使用受控制的函数,不直接使用原函数
只要在第二步保证只转变一次,使结果不变(useMemo),那么 不变的callback 这个优点也就有了。
各位已经可以按照上面的思路来实现一个自己的方案了!
广告
笔者已经照此思路实现了一版,并已经在新项目中使用、上线。新项目中,全部都是函数组件,组件本身不管复杂与否都非常简洁明了!
版本特点:
- 体外函数支持普通函数、异步函数、GeneratorFunction、AsyncGeneratorFunction
- 支持yield一个Promise,并等待promise执行完毕
- 完整的TS支持
- 充分的测试用例
如果各位觉得自己已清楚熟悉方案原理,不想浪费时间去实现一版,可以直接使用笔者的实现版本,或者参考笔者的实现。
GitHub地址:
github.com/elvinzhu/rf… (欢迎star,助笔者实现千星梦 ^_^)
npm包:
npm install rfc-state --save
复制代码
最后
感谢阅读,如有错误之处或者问题需要探讨,评论区见。