自从React 16.8发布Hook以后,笔者已经在实际项目中使用Hook快一年了,虽然Hook在使用中存在着一些坑,可是总的来讲它是一个很好的功能,特别是在减小模板代码和提升代码复用率这些方面特别有用。为了让更多的人了解和使用Hook,我决定写一系列和Hook相关的文章,本篇文章就是这个系列的第一篇,主要和你们聊一下React为何须要Hook。javascript
对于React或者其它的基于Component的框架来讲,页面是由一个个UI组件构成的。独立的组件能够在同一个项目中甚至不一样项目中进行复用,这十分有利于前端开发效率的提升。但是除了UI层面上的复用,一些状态相关(stateful)或者反作用相关(side effect)的非UI逻辑在不一样组件之间复用起来却十分困难。对于React来讲,你可使用高阶组件(High-order Component)或者renderProps的方法来复用这些逻辑,但是这两种方法都不是很好,存在各类各样的问题。若是你以前没有复用过这些非UI逻辑的话,咱们能够先来看一个高阶组件的例子。html
假如你在开发一个社交App的我的详情页,在这个页面中你须要获取并展现当前用户的在线状态,因而你写了一个叫作UserDetail的组件:前端
class UserDetail extends React.Component {
state = {
isOnline: false
}
handleUserStatusUpdate = (isOnline) => {
this.setState({ isOnline })
}
componentDidMount() {
// 组件挂载的时候订阅用户的在线状态
userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
}
componentDidUpdate(prevProps) {
// 用户信息发生了变化
if (prevProps.userId != this.props.userId) {
// 取消上一个用户的状态订阅
userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
// 订阅下一个用户的状态
userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
}
}
componentWillUnmount() {
// 组件卸载的时候取消状态订阅
userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
}
render() {
return (
<UserStatus isOnline={this.state.isOnline}> ) } } 复制代码
从上面的代码能够看出其实在UserDetail组件里面维护用户状态信息并非一件简单的事情,咱们既要在组件挂载和卸载的时候订阅和取消订阅用户的在线状态,并且还要在用户id发生变化的时候更新订阅内容。所以若是另一个组件也须要用到用户在线状态信息的话,做为一个优秀如你的程序员确定不想简单地对这部分逻辑进行复制和粘贴,由于重复的代码逻辑十分不利于代码的维护和重构。接着让咱们看一下如何使用高阶组件的方法来复用这部分逻辑:java
// withUserStatus.jsx
const withUserStatus = (DecoratedComponent) => {
class WrapperComponent extends React.Component {
state = {
isOnline: false
}
handleUserStatusUpdate = (isOnline) => {
this.setState({ isOnline })
}
componentDidMount() {
// 组件挂载的时候订阅用户的在线状态
userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
}
componentDidUpdate(prevProps) {
// 用户信息发生了变化
if (prevProps.userId != this.props.userId) {
// 取消上一个用户的状态订阅
userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
// 订阅下一个用户的状态
userService.subscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
}
}
componentWillUnmount() {
// 组件卸载的时候取消状态订阅
userService.unSubscribeUserStatus(this.props.userId, this.handleUserStatusUpdate)
}
render() {
return <DecoratedComponent isOnline={this.stateIsOnline} {...this.props} /> } } return WrapperComponent } 复制代码
在上面的代码中咱们定义了用来获取用户在线状态的高阶组件,它维护了当前用户的在线状态信息并把它做为参数传递给被装饰的组件。接着咱们就可使用这个高阶组件来重构UserDetail组件的代码了:react
import withUserStatus from 'somewhere'
class UserDetail {
render() {
return <UserStatus isOnline={this.props.isOnline}> } } export default withUserStatus(UserDetail) 复制代码
咱们能够看到使用了withUserStatus高阶组件后,UserDetail组件的代码一会儿变得少了不少,如今它只须要从父级组件中获取到isOnline参数进行展现就好。并且这个高阶组件能够套用在其它任何须要获取用户在线状态信息的组件上,你不再须要在前端维护同样的代码了。git
这里要注意的是上面的高阶组件封装的逻辑和UI展现没有太大关系,它维护的是用户在线状态信息的获取和更新这些和外面世界交互的side effect,以及用户状态的存储这些和组件状态相关的逻辑。虽然看起来彷佛代码很优雅,不过使用高阶组件来封装组件的这些逻辑其实会有如下的问题:程序员
withAuth(withRouter(withUserStatus(UserDetail)))
。这种嵌套写法的高阶组件可能会致使不少问题,其中一个就是props丢失的问题,例如withAuth传递给UserDetail的某个prop可能在withUserStatus组件里面丢失或者被覆盖了。若是你使用的高阶组件都是本身写的话还好,由于调试和修改起来都比较简单,若是你使用的是第三方的库的话就很头痛了。和高阶组件相似,renderProps也会存在一样的问题。基于这些缘由,React须要一个新的用来复用组件之间非UI逻辑的方法,因此Hook就这么诞生了。总的来讲,Hook相对于高阶组件和renderProps在复用代码逻辑方面有如下的优点:github
除了用来替代难用的HOC和renderProps来解决组件非UI逻辑复用的问题以外,其实Hook还解决了如下这些问题。编程
在上面UserDetail组件中咱们将获取用户的在线状态
这个side effect的相关逻辑分散到了componentDidMount
,componentWillUnmount
,componentDidUpdate
三个生命周期函数中,这些互相关联的逻辑被分散到不一样的函数中会致使bug的发生和产生数据不一致的状况。除了这个,咱们还可能会在组件的同一个生命周期函数放置不少互不关联的side effect逻辑。举个例子,若是咱们想在用户查看某个用户的详情页面的时候将浏览器当前标签页的title改成当前用户名的话,就须要在组件的componentDidMount生命周期函数里面添加document.title = this.props.userName
这段代码,但是这段代码和以前订阅用户状态的逻辑是互不关联的,并且随着组件的功能变得愈来愈复杂,这些不关联而又放在一块儿的代码只会变得愈来愈多,因而你的组件逐渐变得难以测试。因而可知Class Component的生命周期函数并不适合用来管理组件的side effect逻辑。浏览器
那么这个问题Hook又是如何解决的呢?因为每一个Hook都是一个函数,因此你能够将和某个side effect相关的逻辑都放在同一个函数(Hook)里面(useEffect Hook)。这种作法有不少好处,首先关联的代码都放在一块儿,能够十分方便代码的维护,其次实现了某个side effect的Hook还能够被不一样的组件进行复用来提升开发效率。举个例子,咱们就能够将改变标签页title的逻辑封装在一个自定的Hook中,若是其它组件有相同逻辑的话就可使用这个Hook了:
// 自定义Hook
function useTabTitle(title) {
React.useEffect(() => {
document.title = title
}, [title])
}
// UserDetail中使用useTabTitle Hook
function UserDetail = (props) => {
useTabTitle(props.userName)
...
}
复制代码
这个复用side effect的功能实际上是一个十分强大的功能,你能够检查一下你如今写的项目代码,确定有不少组件的side effect是能够封装成Hook的。封装成Hook的side effect不只仅能够在某一个项目中使用,还能够在不一样项目中复用,这对咱们的开发效率确定会有很大的提高。
其实Class Component除了生命周期函数不适合side effect的管理以外,还有一些其它的问题。
首先Class Component对开发者不友好。若是你要使用Class Component首先你得理解JS里面的this是怎么使用的,它的使用方法其实和其余语言有很大的区别。因为JS自己的缘由,在Class Component中你要手动为注册的event listener绑定this,否则就会报this is undefined
的错误,早期的React玩家确定体验过每一个事件监听函数都要手动绑定this的酸爽感受,乏味并且容易引起bug,这个问题直到class properties出来以后才有所改善。
class UserDetail extends React.Component {
constructor(props) {
super(props)
this.handlerUserStatusUpdate = this.handleUserStatusUpdate.bind(this)
...
}
}
复制代码
除了对开发者不友好,Class Component对机器也很不友好。例如Class Component的生命周期函数很难被minified。其次,Class Component的存在可能会阻碍React后面的发展。举个例子,随着新的理念 - Compiler as Framework的兴起,一些诸如Svelte, Angular和Glimmer的框架将框架的概念放到了编译时以去除production code里面的runtime代码来加快应用的首屏加载速度,这个方案已经开始被逐渐采纳了,并且将来有可能会成为潮流。若是你们不是很了解Compiler as Framework理念的话,能够看个人另一篇文章:Svelte 3 初学者彻底指南。React已经存在了5年,它若是想要继续存在多五年的话也要跟上这个潮流,出于这个缘由,React团队和Prepack团队进行了一些和Compiler as Framework相关的尝试,并且就目前实验的结果来讲这个思路有很大的想象空间。不过在这个过程当中React的开发者也发现了一个严重的问题,那就是开发者可能会以一种很是规的模式来使用Class Component,而这些模式会下降这个方案带来的优化效果。
所以React要想获得进一步的发展的话,就必须让开发者更多地使用Function Component而不是Class Component。而开发者偏向于使用Class Component而不是Function Component的一个主要缘由是Function Component没有状态管理和生命周期函数等功能。Hook出来后这个问题就不存在了,由于开发者可使用useState Hook来在Function Component使用state以及useEffect Hook来实现一些和生命周期函数相似的功能。最重要的是,React将全部复杂的实现都封装在框架里面了,开发者无需学习函数式编程和响应式编程的概念也能够很好地使用Hook来进行开发。
本篇文章我主要论述了React为啥要有Hook,总的来讲是如下三个缘由:
若是你有其余的补充或者以为我有什么地方说得不对的话能够在评论区和我一块儿探讨,在后面一篇文章中我将会为你们深刻介绍一些经常使用的Hook。
文章始发于个人我的博客
欢迎关注公众号进击的大葱一块儿学习成长