hooks vs class component 之争

我与疫情

2020 转眼已来到3月,但疫情的突袭,让这个春节迟迟没有开始,也无法结束。这段时间看似是充电自我提高的大好时光,但家国情怀深厚的我为疫情真的是操碎了心,时不时都要看看哪里数据猛增了,哪里暴发了。结束一个月的在家办公,带着口罩在公司上班,状态稍有好转,注意力终归回到了技术。 前端

最近在组里推BFF Node接入与微前端改造,看到了组里各式各样实现地业务代码(组件),有激进开放的整个页面都用hooks实现,有沉迷于过去的停留于redux + saga + model的类组件写法,固然更多的是类组件中的子组件参和几个hooks。我带着好奇之心去goggle了一下这两个谁胜一筹,因而有了此文。react

hooks vs class

网上看了不少大佬观点,但脱离业务的争论都是耍流氓。做为眼见为实躺坑无数的练习生,我决定用事实说话。说那么多干吗,上代码:
慢...,先说说接下来要干的事情:git

  • 进入一个列表页面,首次进入,发起请求,获取列表数据;
  • 列表有搜索框,支持条件搜索和重置;
  • 列表支持翻页;
  • 页面弹出一个对话框,用于,新增,修改数据;
  • 修改数据时,须要发起一个请求,去获取详情;

发现没,这就是一个标准的CRUD,页面大体长这样:
image.pnggithub

肤浅的看-表面

接下来,列一下两种方式实现,页面的大体结构,橙色方框内为Hook重构后的页面结构:
image.png
Hook重构后代码变化主要在index.js,删去了对Dva的Model依赖,转由Hook本身管理数据, 代码对比大体以下:
image.png
看代码,主要是数据层的实现有变化,UI保持一致。当页面跑起来,若是不看面包屑的话,也很难分辨谁是Hooks组件的实现,谁是类组件的实现,因此表面的看,是看不出谁高谁低的。spring

仔细的看-性能

毕竟人的肉眼,只有当低于30fps时,才能明显的观察到变化;因此要想客观的对比性能,得依赖Chrome的性能分析(Performerce);这里作两个对比:chrome

  1. 从其余菜单(主页)跳转到列表页
  2. 从列表页到打开详情编辑Modal;

跳转到列表页
image.pngredux

打开编辑页
image.png缓存

为保证试验结果严谨,我尽可能保证惟一性变量原则、多(san)次、与减小本身手抖的次数,但与自动化测试仍是有较大差距,十几毫秒的偏差再所不免。我观察了列表页切换页的两个图,除开请求的波动于手动测试的偏差,两个页面从点击到请求再到页面渲染,时间相差无几,甚至调用栈都是惊人的类似(装逼的说法就是:从原理上分析,也应该是类似的:dispatch + diff + render)。
也观察了详情编辑Modal打开两个图,屡次测试,其打开速度hooks稍暂上风,因为这一块的实现逻辑有比较大的差异(class组件的数据获取是在最外层,获取完而后依次向里传递;而hooks则是从外层获取到id后,组件内部直接发起请求获取数据),因此二者火焰图也有比较大的差别;但从页面渲染的角度总体感受差异不大。 性能优化

最后我得出的见解是:Hooks更可能是一种管理数据的手段,与class相比,并无什么性能上的优点,更多的主动权,在编写代码的人手里,就像我驾校老师爱说的那句狗屁不通的谚语:再好的车,给这个二傻子开,都能开熄火。关于更多,能够关注B乎讨论: React hooks 和 Class Component 的性能哪个更好?
若是对个人测试有疑惑,能够本身动手,我提供示例项目:antd

我我的始终同意:框架只是实现业务的手段,在使用成熟的框架前提下,页面的性能彻底由司机掌控。(我我的有个观点就是:Vue是自动档,React更像手动档

hooks:useRequest

要想彻底脱离redux或mobx,简单使用Hooks中的useState或useEffect来完成页面,其难度仍是很大且很难管理,毕竟页面大多数数据源都来自异步请求,因此封装一个useRequest hooks是势在必要的,并且Hooks最大的优点就是逻辑复用。如下将分享部门封装useRequest组件的思考过程,其思路参考于Apollo-Graphqlreact-hooks项目。先看示例代码(上面列表页的部分代码):

export default function Root() {
 const [search, onSearch, onReset] = usePagination({});

 // 请求:admin.closertb.site/rule/query接口
 const { data = {}, loading, error } = useRequest('/rule/query', search);

 const { datas, total } = data;
 const tableProps = {
   search,
   datas,
   fields,
   onSearch,
   total,
   loading,
 };

 const searchProps = {
   fields: searchFields,
   search,
   onReset,
   onSearch,
 };
 return (
   <div>
     <WithSearch {...searchProps} />
     <div className="pageContent">
       <EnhanceTable {...tableProps} />
     </div>
   </div>
 );
}

以上就是一个最多见的useRequest hook应用,用很是简短的代码替换了Dva中的路由监听(subscription), 异步请求(effect),数据更新(reducer)等一连串逻辑;下图是一个简单的流程示意图:
image.png

根据这个示意图,老司机应该就大概知道怎么实现的了:

  • 运用useRef 来缓存请求实例,即每一个useRequest仅建立一个query实例,页面更新时,沿用已经存在的示例;
  • 运用useMemo作计算,判断请求参数是否更新;
  • 运用useEffect, 用useMemo计算结果做为依赖,判断是否发起请求;
  • 固然,更新页面,采用了一个自增的useReducer;

如今留下的惟一疑问就是,发布订阅怎么实现的,看一下代码:

// query 示例其中的两个核心方法:
  startQuery() {
    const { url, body, forceUpdate, result } = this;
    if (this.status > STATUS.fetch) {
      return;
    }
    // 状态反转为请求中
    this.status = STATUS.fetch;
    result.loading = true;
    // 发起请求
    this.request(url, body).then((data) => {
      // 更新结果
      result.data = data;
      result.loading = false;
      this.status = STATUS.success;
      // 更新订阅
      forceUpdate();
    }).catch((error) => {
      this.status = STATUS.error;
      result.error = error;
      result.loading = false;
      forceUpdate();
    });
  }

  execute() {
    // skip:是否禁用查询
    const { result, options: { skip = false } } = this;
    !skip && this.startQuery();

    // 当skip 为true 时,说明没有查询结果,因此不能用上次的查询结果来作过渡
    return Object.assign({
      error: undefined,
      data: undefined,
      loading: !skip,
      forceUpdate: this.forceQuery,
    }, skip ? {} : result);
  }

是否是有种恍然大悟,并无什么发布订阅的具体实现,彻底依赖于Promise对象隐式的发布订阅,而forceUpdate的实现则依赖于useReducer:

const [tick, forceUpdate] = useReducer(x => x + 1, 0);

从去年用Graphql写完本身的博客,发现Apollo的useQuery这个hooks彩蛋,就一直有想法去实现一个useRequest hooks,在去年的一次需求推动中,强迫本身去编写了这个组件。收获仍是很大的,在咱们团队中已经有必定尝试,固然还有很大的拓展空间。从这个组件的编写历程,我也再一次体验了开手动挡,司机经验的重要性,拿我本身挖到的一个坑举例:

cleanup() {
    this.result.data = undefined;
    this.result.loading = false;
    this.result.error = undefined;
  }

上面一段代码,是每次请求完成后,页面更新后, 有一个useEffect反作用会执行query.cleanup()来保证请求示例回到初始状态。但就这样一段代码,引发了极大的性能问题:

事件唤起查询时,页面会抖动,开始我觉得是我写的hooks的组件不如 react-redux那么多性能优化,其实当时在猜疑是否是hooks的性能问题。

但后面经过应用chrome performance,观测到其抖动,是因为this.result.data = undefined形成的,用一个示意图表示:
image.png
大概意思就是,若是当前页面有数据,再次发起查询时,列表除了当前更新为loading状态,当前10天数据也会被清除,至关于列表作了一次diff并render;请求完成后,loading状态消失,更新列表,又作了一次diff并render.因此会形成抖动。后面完善代码后,就和下面正常的阶跃曲线同样,只有一次阶跃,列表实际只会作一次render.

这次实现是基于团队现有的http库来作的,这个库的原理在之前的一篇文章讲过:边看边写:基于Fetch仿洋葱模型写一个Http构造类

若是你感兴趣,能够在个人github看到:

结语

没啥想总结的,愿疫情早日过去。愿口罩早日摘下,愿火锅早日成为生活的平常。

对了,Antd4.0已经到来,Form表单基本被重写,这意味着我组件库Antd-doddle, 又得作一次大的升级了!!!!cd

相关文章
相关标签/搜索