您当前的位置:首页 > 电脑百科 > 电脑知识

React Hooks的丑陋一面

时间:2020-11-03 10:02:30  来源:  作者:
React Hooks的丑陋一面

 

在这篇文章中,我将分享我对React Hooks的观点,正如这篇文章的标题所暗示的那样,我不是一个忠实的粉丝。

让我们来分析一下React官方的文档中描述的放弃类而使用钩子的动机。

动机1:class令人困惑

我们发现,class可能是学习React的一大障碍,你必须了解 this 在JAVAScript中的工作方式,这与大多数语言中的工作方式截然不同。你必须记住要绑定事件处理程序,代码会非常啰嗦,React中函数和类组件之间的区别,以及何时使用每个组件,甚至在有经验的React开发人员之间也会导致分歧。

好吧,我可以同意 this 在你刚开始使用JavaScript的时候可能会有点混乱,但是箭头函数解决了混乱,把一个已经被Typescript开箱即用支持的第三阶段功能称为“不稳定的语法建议”,这纯粹是煽动性的。React团队指的是class字段语法,该语法已经被广泛使用并且可能很快会得到正式支持:

class Foo extends React.Component {
  onPress = () => {
    console.log(this.props.someProp);
  }

  render() {
    return <Button onPress={this.onPress} />
  }
}

如你所见,通过使用class字段箭头函数,你无需在构造函数中绑定任何内容,并且它始终指向正确的上下文。

如果Class令人困惑,那么对于新的钩子函数我们能说些什么呢?钩子函数不是常规函数,因为它具有状态,看起来很奇怪的 this(又名 useRef ),并且可以具有多个实例。但这绝对不是类,介于两者之间,从现在开始,我将其称为 Funclass。那么,对于人类和机器而言,那些Funclass会更容易吗?我不确定机器,但我真的不认为Funclass从概念上比类更容易理解。

类是一个众所周知的思想概念,每个开发人员都熟悉 this 的概念,即使在javascript中也有所不同。另一方面,Funclass是一个新概念,一个很奇怪的概念。它们让人感觉更神奇,而且它们过于依赖惯例而不是严格的语法。你必须遵循一些严格而奇怪的规则,你需要小心你的代码放在哪里,而且有很多陷阱。还要准备好一些可怕的命名,比如 useRef( this 的花哨名字)、useEffect、useMemo、useImperativeHandle(说什么呢?)等等。

类的语法是为了处理多实例的概念和实例范围的概念(this 的确切目的)而专门发明的。Funclass只是一种实现相同目标的奇怪方式,许多人将Funclass与函数式编程相混淆,但Funclass实际上只是变相的类。类是一个概念,而不是语法。

在React中,函数和类组件之间的区别,以及何时使用每一种组件,甚至在有经验的React开发人员之间也会产生分歧。

到目前为止,这种区别非常明显——如果需要状态或生命周期方法,则使用类,否则,使用函数或类实际上并不重要。就我个人而言,我很喜欢这样的想法:当我偶然发现一个函数组件时,我可以立即知道这是一个没有状态的“哑巴组件”。遗憾的是,随着Funclasses的引入,情况不再是这样了。

动机2:很难在组件之间重用有状态逻辑

具有讽刺意味吗?至少在我看来,React最大的问题是它没有提供一个开箱即用的状态管理方案,让我们对应该如何填补这个空白的问题争论了很久,也为Redux等一些非常糟糕的设计模式打开了一扇门。所以在经历了多年的挫折之后,React团队终于得出了一个结论:组件之间很难共享有状态逻辑......谁能想到呢?

无论如何,勾子会使情况变得更好吗?答案是不尽然。钩子不能和类一起工作,所以如果你的代码库已经用类来编写,你还是需要另一种方式来共享有状态的逻辑。另外,钩子只解决了每个实例逻辑共享的问题,但如果你想在多个实例之间共享状态,你仍然需要使用stores和第三方状态管理解决方案,正如我所说,如果你已经使用它们,你并不真正需要钩子。

所以,与其只是治标不治本,或许React是时候行动起来,实现一个合适的状态管理工具,同时管理全局状态(stores)和本地状态(每个实例),从而彻底扼杀这个漏洞。

动机3:复杂的组件变得难以理解

如果你已经在使用stores,这种说法几乎没有意义,让我们看看为什么。

class Foo extends React.Component {
  componentDidMount() {
    doA(); 
    doB(); 
    doC();
  }
}

在这个例子中,你可以看到,我们可能在 componentDidMount 中混合了不相关的逻辑,但这是否会使我们的组件膨胀?不完全是。整个实现位于类之外,而状态位于store中,没有store 所有状态逻辑都必须在类内部实现,而该类确实会臃肿。但看起来React又解决了一个问题,这个问题大多存在于一个没有状态管理工具的世界里。实际上,大多数大型应用程序已经在使用状态管理工具,并且该问题已得到缓解。另外,在大多数情况下,我们也许可以将这个类分解成更小的组件,并将每个 doSomething() 放在子组件的 componentDidMount 中。

使用Funclass,我们可以编写如下代码:

function Foo() {
  useA(); 
  useB(); 
  useC();
}

看起来有点干净,但是是吗?我们还需要在某个地方写3个不同的useEffect钩子,所以最后我们要写更多的代码,看看我们在这里做了什么——有了类组件,你可以一目了然地知道组件在mount上做什么。在Funclass的例子中,你需要按照钩子并尝试搜索带有空依赖项数组的 useEffect,以了解组件在mount上做什么。生命周期方法的声明性本质上是一件好事,我发现研究Funclasss的流程要困难得多。我见过很多案例是Funclasses让开发者更容易写出糟糕的代码,我们后面会看到一个例子。

但是首先,我必须承认 useEffect 有一些好处,请看以下示例:

useEffect(() => {
  subscribeToA();
  return () => {
    unsubscribeFromA();
  };
 }, []);

useEffect 钩子让我们将订阅和退订逻辑配对在一起。这其实是一个非常整洁的模式,同样的,把 componentDidMount 和 componentDidUpdate 配对在一起也是如此。以我的经验,这些情况并不常见,但它们仍然是有效的用例,在这里 useEffect 确实很有用。问题是,为什么我们必须使用Funclass才能获得 useEffect?为什么我们的Class不能有类似的东西?答案是我们可以:

class Foo extends React.Component {
   someEffect = effect((value1, value2) => {
     subscribeToA(value1, value2);
     return () => {
        unsubscribeFromA();
     };
   })
   render(){ 
    this.someEffect(this.props.value1, this.state.value2);
    return <Text>Hello world</Text>   
   }
}

effect 函数将记住给定的函数,并且仅当其参数之一已更改时才会再次调用它。通过从我们的render函数内部触发效果,我们可以确保它在每次渲染/更新时都被调用,但只有当它的一个参数被改变时,给定的函数才会再次运行,所以我们在结合 componentDidMount 和 componentDidUpdate 方面实现了类似 useEffect 的效果,但遗憾的是,我们仍然需要在 componentWillUnmount 中手动进行最后的清理。另外,从render内调用效果函数也有点丑。为了得到和useEffect完全一样的效果,React需要增加对它的支持。

最重要的是 useEffect 不应该被认为是进入funclass的有效动机,它本身就是一个有效的动机,也可以为类实现。

动机4:性能

React团队说类很难优化和最小化,funclass应该以某种方式改进,关于这件事,我只有一件事要说——给我看看数字

我至今找不到任何论文,也没有我可以克隆并运行以比较Funclasses VS Class的性能的基准演示应用程序。事实上,我们没有看到这样的演示并不奇怪——Funclasses需要以某种方式实现这个功能(如果你喜欢的话,也可以用Ref),所以我很期待那些让类难以优化的问题,也会影响到Funclasses。

不管怎么说,所有关于性能的争论,在不展示数据的情况下实在是一文不值,所以我们真的不能把它作为论据。

动机5:Funclass不太冗长

你可以找到很多通过将Class转换为Funclass来减少代码的例子,但大多数甚至所有的例子都利用了 useEffect 钩子,以便将 componentDidMount 和 componentWillUnmount 结合在一起,从而达到极大的效果。

但正如我前面所说,useEffect 不应该被认为是Funclass的优势,如果忽略它所实现的代码减少,那么只会留下非常小的影响。而且,如果你尝试使用 useMemo,useCallback 等来优化Funclass,你甚至可能得到比等效类更冗长的代码。

当比较小而琐碎的组件时,Funclasses毫无疑问地赢了,因为类有一些固有的模板,无论你的类有多小你都需要付出。但在比较大的组件时,你几乎看不出差别,有时正如我所说,类甚至可以更干净。

最后,我不得不对 useContext 说几句:useContext其实比我们目前原有的类的context API有很大的改进。但是再一次,为什么我们不能为类也有这样漂亮而简洁的API呢? 为什么我们不能做这样的事情。

//inside "./someContext" :
export const someContext = React.Context({helloText: 'bla'});

//inside "Foo":
import {someContext} from './someContext';

class Foo extends React.component {
   render() {
      <View>
        <Text>{someContext.helloText}</Text>
      </View>
   }
}

当上下文中的 helloText 发生变化时,组件应该重新渲染以反映这些变化。就是这样,不需要丑陋的高阶组件(HOC)。

那么,为什么React团队选择只改进useContext API而不是常规content API?我不知道,但这并不意味着Funclass本质上更干净。这意味着React应该通过为类实现相同的API改进来做得更好。

因此,在提出有关动机的问题之后,让我们看一下我不喜欢的有关Funclass的其他内容。

隐藏的副作用

在Funclasses的 useEffect 实现中,最让我困扰的一件事,就是没有弄清楚某个组件的副作用是什么。对于类,如果你想知道一个组件在挂载时做了什么,你可以很容易地检查 componentDidMount 中的代码或检查构造函数。如果你看到一个重复的调用,你可能应该检查一下 componentDidUpdate,有了新的 useEffect 钩子,副作用可以深深地嵌套在代码中。

假设我们检测到一些不必要的服务器调用,我们查看可疑组件的代码,然后看到以下内容:

const renderContacts = (props) => {
  const [contacts, loadMoreContacts] = useContacts(props.contactsIds);
  return (
    <SmartContactList contacts={contacts}/>
  )
}

这里没什么特别的,我们应该研究 SmartContactList,还是应该深入研究 useContacts?让我们深入研究一下 useContacts 吧:

export const useContacts = (contactsIds) => {
  const {loadedContacts, loadingStatus}  = useContactsLoader();
  const {isRefreshing, handleSwipe} = useSwipeToReresh(loadingStatus);
  // ... many other useX() functions
  useEffect(() => {
    //** 很多代码,都与一些加载联系人的动画有关。*//
  
  }, [loadingStatus]);
  
  //..rest of code
}

好的,开始变得棘手。隐藏的副作用在哪里?如果我们深入研究 useSwipeToRefresh,我们将看到:

export const useSwipeToRefresh = (loadingStatus) => {
  // ..lot's of code
  // ...
  
  useEffect(() => {
    if(loadingStatus === 'refresing') {
       refreshContacts(); // bingo! 我们隐藏的副作用
    }  
  }); //<== 我们忘记了依赖项数组!
}

我们发现了我们的隐藏效果,refreshContacts 会在每个组件渲染时意外地调用fetch contacts。在大型代码库和某些结构不良的组件中,嵌套的 useEffect 可能会造成麻烦。

我并不是说你不能用类编写糟糕的代码,但是Funclasses更容易出错,而且没有严格定义生命周期方法的结构,更容易做糟糕的事情。

膨胀的API

通过在类的同时增加钩子API,React的API实际上增加了一倍。现在每个人都需要学习两种完全不同的方法,我必须说,新API比旧API晦涩得多。一些简单的事情,如获得之前的props和state,现在都成了很好的面试材料。你能写一个钩子获得之前得 props 在不借助google的情况下?

像React这样的大型库必须非常小心地在API中添加如此巨大的更改,这样做的动机甚至是不合理的。

缺乏说明性

在我看来,Funclass比类更混乱。例如,要找到组件的切入点就比较困难——用classes只需搜索 render 函数,但用Funclasses就很难发现主return语句。另外,要按照不同的 useEffect 语句来理解组件的流程是比较困难的,相比之下,常规的生命周期方法会给你一些很好的提示,让你知道自己的代码需要在哪里寻找。如果我正在寻找某种初始化逻辑,我将跳转(VSCode中的cmd + shift + o)到 componentDidMount,如果我正在寻找某种更新机制,则可能会跳到 componentDidUpdate 等。通过Funclass,我发现很难在大型组件内部定位。

约定驱动的API

钩子的主要规则(可能也是最重要的规则)之一是使用前缀约定。

就是感觉不对

你知道有什么不对劲的感觉吗?这就是我对钩子的感觉。有时我能准确地指出问题所在,但有时只是一种普遍的感觉,即我们走错了方向。当你发现一个好的概念时,你可以看到事情是如何很好地结合在一起的,但是当你在为错误的概念而苦恼的时候,发现你需要添加更多更具体的东西和规则,才能让事情顺利进行。

有了钩子,就会有越来越多奇怪的东西跳出来,有更多“有用的”钩子可以帮助你做一些琐碎的事情,也有更多的东西需要学习。如果我们需要这么多的utils在我们的日常工作中,只是为了隐藏一些奇怪的复杂,这是一个巨大的迹象,说明我们走错了路。

几年前,当我从Angular 1.5转到React时,我惊讶于React的API是如此简单,文档是如此的薄。Angular曾经有庞大的文档,你可能要花上几天的时间才能涵盖所有内容——消化机制、不同的编译阶段、transclude、绑定、模板等等。光是这一点就给我很大的启示,而React它简洁明了,你可以在几个小时内把整个文档看一遍就可以了。在第一次,第二次以及以后的所有次尝试使用钩子的过程中,我发现自己有义务一次又一次地使用文档。

总结

我讨厌成为聚会的扫兴者,但我真的认为Hooks可能是React社区发生的第2件最糟糕的事情(第一名仍然由Redux占据)。它给已经脆弱的生态系统增加了另一场毫无用处的争论,目前尚不清楚钩子是否是推荐的使用方式,还是只是另一个功能和个人品味的问题。

我希望React社区能够醒来,并要求在Funclass和class的功能之间保持平衡。我们可以在类中拥有更好的Context API,并且可以为类提供诸如useEffect之类的东西。如果需要,React应该让我们选择继续使用类,而不是通过仅为Funclass添加更多功能而强行杀死它而将类抛在后面。

另外,早在2017年底,我就曾以《Redux的丑陋面》为题发表过一篇文章,如今连Redux的创造者Dan Abramov都已经承认Redux是一个巨大的错误。

React Hooks的丑陋一面

 

只是历史在重演吗?时间会证明一切。

无论如何,我和我的队友决定暂时坚持用类,并使用基于Mobx的解决方案作为状态管理工具。我认为,在独立开发人员和团队工作人员之间,Hooks的普及率存在很大差异——Hooks的不良性质在大型代码库中更加明显,你需要在该代码库中处理其他人的代码。我个人真的希望React能把 ctrl+z 的钩子全部放在一起。

我打算开始着手制定一个RFC,为React提出一个简单、干净、内置的状态管理方案,一劳永逸地解决共享状态逻辑的问题,希望能用一种比Funclasses不那么笨拙的方式。



Tags:React Hooks   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
在这篇文章中,我将分享我对React Hooks的观点,正如这篇文章的标题所暗示的那样,我不是一个忠实的粉丝。让我们来分析一下React官方的文档中描述的放弃类而使用钩子的动机。动...【详细内容】
2020-11-03  Tags: React Hooks  点击:(80)  评论:(0)  加入收藏
"Hooks 到底是个啥玩意儿???"你是不是有这样的疑惑?在你自认为已经了解了差不多 React 所有的内容的时候,Hooks 就这么出现了。这就是前端开发人员的日常,技术从未停止更新。学习...【详细内容】
2020-07-18  Tags: React Hooks  点击:(40)  评论:(0)  加入收藏
▌简易百科推荐
回答这个问题前首先要了解一度电的定义一度电= 1000W&middot;h ,是一个能量单位,一个功率为1000w的设备持续运作一小时就会消耗1度电。所以台式机多少时间一度电完全取决于台...【详细内容】
2021-12-20  数码榜    Tags:台式电脑   点击:(5)  评论:(0)  加入收藏
你是不是很多时候都会紧急冲向电源插座以拯救笔记本电脑的电量?很多时候由于附近没有方便的插座,一是会误事,二是会导致尴尬。幸运的是,现代笔记本电脑比前辈高效得多。如今,即...【详细内容】
2021-12-16  趣玩公社    Tags:电池   点击:(17)  评论:(0)  加入收藏
电脑在我们生活中的重要性不言而喻。如何保证自己的电脑流畅好用,对于很多用户来说都非常重要。作为一个理科男和IT从业者,对于自己的电脑还是非常在意的,会定期的进行整理,保持...【详细内容】
2021-12-09  小伊评科技    Tags:电脑   点击:(13)  评论:(0)  加入收藏
大家好,我是良许。不管我们使用什么操作系统,无论是 Windows、macOS 还是 Linux ,里面都安装了许多软件、驱动程序和固件。但是,这三者概念有区别呢?我在朋友圈做了个小调查,发现...【详细内容】
2021-11-30  良许Linux    Tags:固件   点击:(10)  评论:(0)  加入收藏
澎湃问吧世界著名密码史学家戴维&middot;卡恩曾说:“人类使用密码的历史几乎与使用文字的时间一样长”,这意味着人类密码领域的较量已近五千年。提及“密码”一词,大多数人会想...【详细内容】
2021-11-24    澎湃新闻  Tags:密码   点击:(16)  评论:(0)  加入收藏
由于去中心化域名和账户体系可以承载应用、网站和用户身份,因此有理由相信,这是 Web3 世界中不可或缺的一环。撰文:潘致雄如果回忆一下初次进行的加密货币转账时的体验,特别是在...【详细内容】
2021-11-03  链闻ChainNews    Tags:ENS   点击:(26)  评论:(0)  加入收藏
 文件系统(File System)是计算机系统必不可少的组成部分,可以说除了部分结构简单的单片机系统之外,文件系统是支撑每一个计算机系统运行的最重要的支撑,无论是操作系统、应用程...【详细内容】
2021-11-01  欧工玩转嵌入式  CSDN  Tags:FAT32   点击:(30)  评论:(0)  加入收藏
SDS 2.0时代进化达尔文的《进化论》认为,地球上现存的所有生物都是自然选择的结果,生物只有适应环境的变化,才能得到生存和进化。回望到企业存储,一个常做常新的行业,从上古结绳...【详细内容】
2021-10-28  ExponTech   企鹅号  Tags:SDS   点击:(35)  评论:(0)  加入收藏
在昨天的微信《远程办公危机四伏,到底该pick谁给你保驾护航?》中介绍了远程员工应该具备的四大安全工具,今天继续分享干货:05 双因子令牌在理想的情况下,每个人都会对所有的关键...【详细内容】
2021-10-26    计算机世界  Tags:远程办公   点击:(32)  评论:(0)  加入收藏
今天几乎所有的数字显示设备都基于某种类型的 RGB(红、绿、蓝)颜色模型。RGB 是表示人类可以看到的大多数颜色的最有效方式(有一些颜色不能很好地产生,但这是另一个讨论)。相机...【详细内容】
2021-10-09  懒懒散散的程序员    Tags:RGB   点击:(49)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条