在这篇文章中,我将分享我对React Hooks的观点,正如这篇文章的标题所暗示的那样,我不是一个忠实的粉丝。javascript
让咱们来分析一下React官方的文档中描述的放弃类而使用钩子的动机。前端
咱们发现,class多是学习React的一大障碍,你必须了解
this
在JavaScript中的工做方式,这与大多数语言中的工做方式大相径庭。你必须记住要绑定事件处理程序,代码会很是啰嗦,React中函数和类组件之间的区别,以及什么时候使用每一个组件,甚至在有经验的React开发人员之间也会致使分歧。
好吧,我能够赞成 this
在你刚开始使用Javascript的时候可能会有点混乱,可是箭头函数解决了混乱,把一个已经被Typescript开箱即用支持的第三阶段功能称为“不稳定的语法建议”,这纯粹是煽动性的。React团队指的是class字段语法,该语法已经被普遍使用而且可能很快会获得正式支持:java
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的引入,状况再也不是这样了。ide
具备讽刺意味吗?至少在我看来,React最大的问题是它没有提供一个开箱即用的状态管理方案,让咱们对应该如何填补这个空白的问题争论了好久,也为Redux等一些很是糟糕的设计模式打开了一扇门。因此在经历了多年的挫折以后,React团队终于得出了一个结论:组件之间很难共享有状态逻辑......谁能想到呢?函数式编程
不管如何,勾子会使状况变得更好吗?答案是不尽然。钩子不能和类一块儿工做,因此若是你的代码库已经用类来编写,你仍是须要另外一种方式来共享有状态的逻辑。另外,钩子只解决了每一个实例逻辑共享的问题,但若是你想在多个实例之间共享状态,你仍然须要使用stores和第三方状态管理解决方案,正如我所说,若是你已经使用它们,你并不真正须要钩子。
因此,与其只是治标不治本,或许React是时候行动起来,实现一个合适的状态管理工具,同时管理全局状态(stores)和本地状态(每一个实例),从而完全扼杀这个漏洞。
若是你已经在使用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的有效动机,它自己就是一个有效的动机,也能够为类实现。
React团队说类很难优化和最小化,funclass应该以某种方式改进,关于这件事,我只有一件事要说——给我看看数字。
我至今找不到任何论文,也没有我能够克隆并运行以比较Funclasses VS Class的性能的基准演示应用程序。事实上,咱们没有看到这样的演示并不奇怪——Funclasses须要以某种方式实现这个功能(若是你喜欢的话,也能够用Ref),因此我很期待那些让类难以优化的问题,也会影响到Funclasses。
无论怎么说,全部关于性能的争论,在不展现数据的状况下实在是一文不值,因此咱们真的不能把它做为论据。
你能够找到不少经过将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
,有了新的 useEffec
t钩子,反作用能够深深地嵌套在代码中。
假设咱们检测到一些没必要要的服务器调用,咱们查看可疑组件的代码,而后看到如下内容:
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,React的API实际上增长了一倍。如今每一个人都须要学习两种彻底不一样的方法,我必须说,新API比旧API晦涩得多。一些简单的事情,如得到以前的props和state,如今都成了很好的面试材料。你能写一个钩子得到以前得 props 在不借助google的状况下?
像React这样的大型库必须很是当心地在API中添加如此巨大的更改,这样作的动机甚至是不合理的。
在我看来,Funclass比类更混乱。例如,要找到组件的切入点就比较困难——用classes只需搜索 render
函数,但用Funclasses就很难发现主return语句。另外,要按照不一样的 useEffect 语句来理解组件的流程是比较困难的,相比之下,常规的生命周期方法会给你一些很好的提示,让你知道本身的代码须要在哪里寻找。若是我正在寻找某种初始化逻辑,我将跳转(VSCode中的cmd + shift + o)到 componentDidMount
,若是我正在寻找某种更新机制,则可能会跳到 componentDidUpdate
等。经过Funclass,我发现很难在大型组件内部定位。
钩子的主要规则(可能也是最重要的规则)之一是使用前缀约定。
你知道有什么不对劲的感受吗?这就是我对钩子的感受。有时我能准确地指出问题所在,但有时只是一种广泛的感受,即咱们走错了方向。当你发现一个好的概念时,你能够看到事情是如何很好地结合在一块儿的,可是当你在为错误的概念而苦恼的时候,发现你须要添加更多更具体的东西和规则,才能让事情顺利进行。
有了钩子,就会有愈来愈多奇怪的东西跳出来,有更多“有用的”钩子能够帮助你作一些琐碎的事情,也有更多的东西须要学习。若是咱们须要这么多的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是一个巨大的错误。
只是历史在重演吗?时间会证实一切。
不管如何,我和个人队友决定暂时坚持用类,并使用基于Mobx的解决方案做为状态管理工具。我认为,在独立开发人员和团队工做人员之间,Hooks的普及率存在很大差别——Hooks的不良性质在大型代码库中更加明显,你须要在该代码库中处理其余人的代码。我我的真的但愿React能把 ctrl+z
的钩子所有放在一块儿。
我打算开始着手制定一个RFC,为React提出一个简单、干净、内置的状态管理方案,一劳永逸地解决共享状态逻辑的问题,但愿能用一种比Funclasses不那么笨拙的方式。
首发于公众号《前端全栈开发者》,第一时间阅读最新文章,会优先两天发表新文章。关注后私信回复:大礼包,送某网精品视频课程网盘资料,准能为你节省很多钱!