Code前端首页关于Code前端联系我们

Vue3 Hooks到底解决了什么问题?新手能不能直接跳过Options API学它?

terry 50分钟前 阅读数 10 #Vue
文章标签 Vue3 HooksOptions API

之前有人问过我,学Vue3是不是必须先啃完Options API,再碰新出的Hooks?还有朋友说Options API写组件写得好好的,为啥突然要换成Hooks这种看起来“乱糟糟”的写法?这些问题其实问得特别实在——毕竟框架的每一次大更新,底层逻辑变了是小事,改变开发者的习惯才是最考验人的,今天咱们就坐下来好好唠唠,不整那些干巴巴的术语,聊点写代码时真真切切的感受。

先说说Options API让很多人头疼的地方

聊Hooks的好处,总绕不开它的“前身”或者说“对照品”Options API,其实Options API不是不好,刚出来的时候它凭借“清晰的职责划分”圈了一大波粉:data放数据、methods放方法、computed放计算属性、watch监听数据、mounted这些钩子处理生命周期……新手刚学Vue的时候,对着固定的格子填代码就行,甚至不用太懂背后的原理,就能写出一个能用的小组件,但这种“清晰”是有代价的,当你的组件稍微复杂一点——比如要做一个带搜索、筛选、分页、实时数据同步的电商商品列表页——问题就全出来了。

第一个问题:逻辑分散,维护像翻乱的抽屉

这个应该是用过Options API写过复杂组件的开发者,最能感同身受的,就拿刚才的商品列表页举例,搜索功能的逻辑会拆在哪里?data里要放searchText、searchResult这些变量;methods里要写handleSearchInput、triggerSearch这些方法;computed里可能会有filteredBySearch的中间计算;watch要监听searchText的防抖变化;如果还要同步搜索状态到URL,mounted里得读取URL参数,beforeRouteUpdate里也要处理路由参数的变化,甚至可能还要加个destroyed清理定时器……明明是“搜索”这一件事,代码却被拆到了五六个不同的选项里,想看全搜索的逻辑,得上下滚动几十甚至上百次,修改的时候也特别容易漏改某个地方,比如你改了触发搜索的防抖时间,可能会忘了同步修改watch里的配置。

有个真实的场景我印象特别深:去年帮一个朋友改代码,他写了一个在线直播的互动组件,里面有点赞、弹幕、送礼、登录验证四个主要功能,我打开组件一看,好家伙,data有80多行,methods有200多行,mounted+beforeUpdate这些钩子加起来快100行,代码总长度快600行了,我想改一下送礼的动画触发逻辑,结果找了十几分钟才找到相关的代码:data里的giftAnimationQueue、giftCurrentIndex,methods里的sendGift、playNextGiftAnimation,mounted里的初始化动画队列监听,watch里监听礼物队列长度变化……那天改完我跟他说,这哪是组件啊,这是把所有东西都塞在一个文件夹里的旧仓库。

第二个问题:逻辑复用难,复制粘贴成常态

在Options API时代,要复用组件逻辑,你有哪些办法?最常见的是mixins,还有就是高阶组件(HOC)、作用域插槽(Scoped Slots)对吧?但这些办法,没有一个是完美的。

先说mixins,mixins的问题简直太多了:第一,命名冲突,如果你用了两个mixins,都定义了一个叫loading的data或者叫handleClick的方法,最后谁覆盖谁全看引入的顺序,调试的时候哭都没地方哭;第二,来源不透明,当你在组件里直接用一个叫getUserInfo的方法或者叫userList的变量时,你根本不知道它是从哪个mixins里来的,得一个个翻引入的mixins文件;第三,耦合度高,如果你的组件只需要mixins里的某一个功能,你也得把整个mixins引入进来,带进来一堆没用的变量和方法,增加了组件的复杂度。

那高阶组件和作用域插槽呢?高阶组件会让你的组件嵌套层级变深,Vue DevTools里看组件树的时候全是一层套一层的HOC,调试起来特别麻烦;作用域插槽虽然解决了命名冲突和来源不透明的问题,但代码写起来特别绕,要定义插槽、传 props、在父组件里用 template 包裹,新手看了直接头大,老手写久了也觉得烦,就举个简单的例子,你想复用“鼠标位置追踪”的逻辑,用作用域插槽的话,得写个MouseTracker组件,里面在data放x、y,methods放updateMousePosition,mounted加监听,destroyed清监听,然后用传出去,父组件里用的时候还要套一层,然后才能用x和y,这还只是个最简单的逻辑,如果是带缓存、带错误处理的异步请求复用,那代码复杂度直接翻倍。

第三个问题:TypeScript支持差,类型推导全靠“any”大法

现在前端开发,TypeScript已经越来越普及了,很多公司的项目都要求必须用TS,但Options API对TS的支持,真的只能用“鸡肋”来形容,你要给data、props、methods这些加类型,得用Vue.extend或者defineComponent,还要配合泛型、this类型断言,写起来特别啰嗦,类型推导也经常出问题,比如你在data里定义了一个变量,然后在methods里用,TS可能会提示“this上没有这个属性”,这时候你要么把整个组件的类型改成any,要么就得给this加上一堆复杂的类型注解,新手根本搞不定,老手也觉得麻烦。

那Vue3 Hooks是怎么解决这些问题的?

Vue3 Hooks其实不是Vue3原创的,它的灵感来自于React Hooks,但Vue团队结合了Vue的响应式原理,做了很多优化,让它比React Hooks更符合Vue开发者的习惯,Vue3的核心Hooks其实就是Composition API里的那些函数,比如ref、reactive、computed、watch、watchEffect、onMounted、onUnmounted等等,现在咱们就来看看,这些Hooks是怎么解决刚才Options API的三个问题的。

第一个解决:逻辑聚合成“功能块”,维护起来像翻分类文件夹

刚才说Options API的代码是“按职责分类”的,那Vue3 Hooks的代码就是“按功能分类”的,还是拿刚才的电商商品列表页举例,用Hooks的话,你可以把搜索功能的所有逻辑,都放在一个叫useSearch的自定义Hooks里;把筛选功能的所有逻辑,放在useFilter里;把分页功能的,放在usePagination里;把实时数据同步的,放在useRealTimeSync里,最后在组件里,只需要引入这些自定义Hooks,调用一下,就能拿到需要的变量和方法,整个组件的代码可能只有几十行,特别清爽。

我去年帮朋友改的那个在线直播互动组件,后来我就是用自定义Hooks重构的,我把点赞的逻辑放在useLike里,弹幕放在useDanmaku里,送礼放在useGift里,登录验证放在useAuth里,重构完之后,原来的600多行组件代码,只剩下不到100行,每个自定义Hooks都是一个独立的文件,每个文件里只有几百行甚至几十行的代码,想看哪个功能的逻辑,直接打开对应的Hooks文件就行,修改起来也特别方便——比如改送礼的动画触发逻辑,只需要动useGift这一个文件就行,根本不会影响其他功能。

可能有人会问,那自定义Hooks怎么写啊?其实特别简单,就是一个普通的JavaScript函数,里面可以用所有的Vue3核心Hooks,最后返回需要暴露给组件的变量和方法就行,比如刚才说的“鼠标位置追踪”的自定义Hooks,用Vue3写的话,代码可能是这样的:

import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
  // 定义鼠标的x、y坐标,用ref包裹,变成响应式数据
  const x = ref(0)
  const y = ref(0)
  // 定义更新鼠标位置的方法
  function updatePosition(e) {
    x.value = e.clientX
    y.value = e.clientY
  }
  // 组件挂载的时候,添加鼠标移动监听
  onMounted(() => {
    window.addEventListener('mousemove', updatePosition)
  })
  // 组件卸载的时候,移除鼠标移动监听,防止内存泄漏
  onUnmounted(() => {
    window.removeEventListener('mousemove', updatePosition)
  })
  // 把需要暴露的变量和方法返回出去
  return { x, y }
}

然后在组件里用的时候,只需要这样:

<template>
  <div>
    <p>鼠标X坐标:{{ x }}</p>
    <p>鼠标Y坐标:{{ y }}</p>
  </div>
</template>
<script setup>
// 引入刚才写的自定义Hooks
import { useMousePosition } from './useMousePosition.js'
// 调用Hooks,拿到x、y
const { x, y } = useMousePosition()
</script>

你看,是不是比作用域插槽简单多了?而且命名也不会冲突——如果你的组件里同时需要两个鼠标位置追踪器,只需要调用两次useMousePosition,然后给返回的变量起不同的名字就行,比如const { x: x1, y: y1 } = useMousePosition(),const { x: x2, y: y2 } = useMousePosition(),完全不会有问题。

第二个解决:逻辑复用超简单,命名和来源全透明

刚才说Options API时代复用逻辑难,mixins、HOC、作用域插槽都有问题,那Vue3的自定义Hooks呢?可以说是完美解决了这些问题。

命名冲突的问题:刚才已经说过了,你可以给返回的变量起不同的名字,或者直接在自定义Hooks里给变量起更有意义的名字,比如useMousePosition里的x、y,你可以改成mouseX、mouseY,这样在组件里同时用多个追踪器的时候,连重命名都不用。

来源不透明的问题:你在组件里用的每个变量和方法,都是从你引入的自定义Hooks里解构出来的,一眼就能看出来它是从哪里来的,比如刚才的x、y,就是从useMousePosition里来的,根本不用翻一堆mixins文件。

第三,耦合度低的问题:你只需要引入你需要的自定义Hooks,不需要引入任何多余的东西,比如你只需要useMousePosition里的x坐标,那你在解构的时候只解构x就行,y根本不会出现在你的组件里,不会增加任何复杂度。

除了刚才说的鼠标位置追踪,自定义Hooks还可以复用很多其他复杂的逻辑,比如带缓存和错误处理的异步请求、表单验证、倒计时、深色模式切换、localStorage/sessionStorage存储、防抖节流、WebSocket连接等等,现在网上已经有很多优秀的Vue3 Hooks库了,比如VueUse,里面有几百个常用的自定义Hooks,你可以直接拿来用,不用自己重复造轮子,能大大提高开发效率。

第三个解决:TypeScript支持超级好,类型推导自动完成

Vue3从一开始就是用TypeScript写的,所以Composition API(也就是Hooks)对TS的支持,可以说是“原生级”的,根本不需要你写太多的类型注解,TS就能自动推导出大部分类型,比如刚才的useMousePosition自定义Hooks,如果你用TS写的话,代码可能是这样的:

import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
  // 这里不用给x、y加类型注解,ref(0)会自动推导出number类型
  const x = ref(0)
  const y = ref(0)
  // 这里给事件参数e加个MouseEvent类型就行,其他的不用管
  function updatePosition(e: MouseEvent) {
    x.value = e.clientX
    y.value = e.clientY
  }
  onMounted(() => {
    window.addEventListener('mousemove', updatePosition)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', updatePosition)
  })
  // 这里也不用给返回值加类型注解,TS会自动推导出{ x: Ref<number>, y: Ref<number> }
  return { x, y }
}

然后在组件里用的时候,TS也能自动推导出x、y的类型是Ref,你在template里或者script里用的时候,都会有代码提示,如果你不小心写错了,比如把x.value改成了x.val,TS会立刻报错,根本不用等到运行的时候才发现问题。

除了自动类型推导,Vue3还提供了很多辅助类型,比如Ref、Reactive、ComputedRef、WatchSource等等,如果你需要更精细的类型控制,也可以自己加,写起来特别流畅,完全不会有Options API时代的那种“鸡肋”感。

新手能不能直接跳过Options API学Vue3 Hooks?

聊完了Hooks的好处,终于到了大家最关心的问题:新手能不能直接跳过Options API学Vue3 Hooks?我的答案是:完全可以,但最好还是先了解一下Options API的核心概念。

为什么说完全可以?因为现在Vue3的官方文档,已经把Composition API作为重点推荐的写法了,甚至在“快速上手”里,直接用的就是Composition API的