当前位置:

hooks怎么样,为什么vue和react都选择它

访客 2023-08-17 1110 0

阅读本文,你将:
  • 初步了解Hooksvuereact的现状

  • 弄懂“为什么我们需要Hooks

    hooks怎么样,为什么vue和react都选择它

  • 进行一些简单的Hooks实践

一、hooks:什么叫大势所趋?

2019年年初,react16.8.x版本正式具备了hooks能力。

2019年6月,尤雨溪在vue/github-issues里提出了关于vue3ComponentAPI的提案。(vuehooks的基础)

除此之外,solid.jspreact等框架,也是开始选择加入hooks大家庭。

可以预见,虽然目前仍然是classComponenthooksapi并驾齐驱的场面,但未来几年里,hooks极有可能取代classComponent成为业内真正的主流。

二、什么是hooks?2.1hooks的定义

"hooks"直译是“钩子”,它并不仅是react,甚至不仅是前端界的专用术语,而是整个行业所熟知的用语。通常指:

比较常见的钩子有:windows系统的钩子能监听到系统的各种事件,浏览器提供的onloadaddEventListener能注册在浏览器各种时机被调用的方法。

以上这些,都可以被称一声"hook"。

但是很显然,在特定领域的特定话题下,hooks这个词被赋予了一些特殊的含义。

react@16.x之前,当我们谈论hooks时,我们可能谈论的是“组件的生命周期”。

但是现在,hooks则有了全新的含义。

react为例,hooks是:

一系列以“use”作为开头的方法,它们提供了让你可以完全避开class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。

简化一下:

一系列方法,提供了在函数式组件中完成开发工作的能力。

(记住这个关键词:函数式组件

import{useState,useEffect,useCallback}from'react';//比如以上这几个方法,就是最为典型的Hooks

而在vue中,hooks的定义可能更模糊,姑且总结一下:

vue组合式API里,以“use”作为开头的,一系列提供了组件复用、状态管理等开发能力的方法。

(关键词:组合式API

import{useSlots,useAttrs}from'vue';import{useRouter}from'vue-router';//以上这些方法,也是vue3中相关的Hook!

如:useSlotsuseAttrsuseRouter等。

但主观来说,我认为vue组合式API其本身就是“vuehooks”的关键一环,起到了reacthooks里对生命周期、状态管理的核心作用。(如onMountedref等等)。

如果按这个标准来看的话,vuereacthooks的定义,似乎都差不多。

那么为什么要提到是以“use”作为开头的方法呢?

2.2命名规范和指导思想

通常来说,hooks的命名都是以use作为开头,这个规范也包括了那么我们自定义的hooks

为什么?

因为(爱情误)约定。

react官方文档里,对hooks的定义和使用提出了“一个假设、两个只在”核心指导思想。(播音腔)

一个假设:假设任何以「use」开头并紧跟着一个大写字母的函数就是一个Hook

第一个只在:只在React函数组件中调用Hook,而不在普通函数中调用Hook。(Eslint通过判断一个方法是不是大坨峰命名来判断它是否是React函数)

第二个只在:只在最顶层使用Hook,而不要在循环,条件或嵌套函数中调用Hook。

因为是约定,因而useXxx的命名并非强制,你依然可以将你自定义的hook命名为byXxx或其他方式,但不推荐。

因为约定的力量在于:我们不用细看实现,也能通过命名来了解一个它是什么。

以上“一个假设、两个只在”总结自react官网:

  • https://zh-hans.reactjs.org/docs/hooks-rules.html

  • https://zh-hans.reactjs.org/docs/hooks-faq.html#what-exactly-do-the-lint-rules-enforce

三、为什么我们需要hooks?

3.1更好的状态复用

怼的就是你,mixin

class组件模式下,状态逻辑的复用是一件困难的事情。

假设有如下需求:

当组件实例创建时,需要创建一个state属性:name,并随机给此name属性附一个初始值。除此之外,还得提供一个setName方法。你可以在组件其他地方开销和修改此状态属性。

更重要的是:这个逻辑要可以复用,在各种业务组件里复用这个逻辑。

在拥有Hooks之前,我首先会想到的解决方案一定是mixin

代码如下:(此示例采用vue2mixin写法)

//混入文件:name-mixin.jsexportdefault{data(){return{name:genRandomName()//假装它能生成随机的名字}},methods:{setName(name){this.name=name}}}//组件:my-component.vue<template><div>{{name}}</div><template><script>importnameMixinfrom'./name-mixin';exportdefault{mixins:[nameMixin],//通过mixins,你可以直接获得nameMixin中所定义的状态、方法、生命周期中的事件等mounted(){setTimeout(()=>{this.setName('Tom')},3000)}}<script>

粗略看来,mixins似乎提供了非常不错的复用能力,但是,react官方文档直接表明:

为什么呢?

因为mixins虽然提供了这种状态复用的能力,但它的弊端实在太多了

弊端一:难以追溯的方法与属性!

试想一下,如果出现这种代码,你是否会怀疑人生:

exportdefault{mixins:[a,b,c,d,e,f,g],//当然,这只是表示它混入了很多能力mounted(){console.log(this.name)//mmp!这个this.name来自于谁?我难道要一个个混入看实现?}}

又或者:

a.jsmixins:[b.js]b.jsmixins:[c.js]c.jsmixins:[d.js]//你猜猜看,this.name来自于谁?//求求你别再说了,我血压已经上来了

弊端二:覆盖、同名?贵圈真乱!

当我同时想混入mixin-a.jsmixin-b.js以同时获得它们能力的时候,不幸的事情发生了:

由于这两个mixin功能的开发者惺惺相惜,它们都定义了this.name作为属性。

这种时候,你会深深怀疑,mixins究竟是不是一种科学的复用方式。

弊端三:梅开二度?代价很大!

仍然说上面的例子,如果我的需求发生了改变,我需要的不再是一个简单的状态name,而是分别需要firstNamelastName
此时name-mixin.js混入的能力就会非常尴尬,因为我无法两次mixins同一个文件。

当然,也是有解决方案的,如:

//动态生成mixinfunctiongenNameMixin(key,funcKey){return{data(){return{[key]:genRandomName()}},methods:{[funcKey]:function(v){this.[key]=v}}}}exportdefault{mixins:[genNameMixin('firstName','setFirstName'),genNameMixin('lastName','setLastName'),]}

确实通过动态生成mixin完成了能力的复用,但这样一来,无疑更加地增大了程序的复杂性,降低了可读性。

因此,一种新的“状态逻辑复用”就变得极为迫切了——它就是Hooks!

Hook的状态复用写法:

//单个name的写法const{name,setName}=useName();//梅开二度的写法const{name:firstName,setName:setFirstName}=useName();const{name:secondName,setName:setSecondName}=useName();

相比于mixins,它们简直太棒了!

  1. 方法和属性好追溯吗?这可太好了,谁产生的,哪儿来的一目了然。
  2. 会有重名、覆盖问题吗?完全没有!内部的变量在闭包内,返回的变量支持定义别名。
  3. 多次使用,没开N度?你看上面的代码块内不就“梅开三度”了吗?

就冲“状态逻辑复用”这个理由,Hooks就已经香得我口水直流了。

3.2代码组织

熵减,宇宙哲学到编码哲学。

项目、模块、页面、功能,如何高效而清晰地组织代码,这一个看似简单的命题就算写几本书也无法完全说清楚。

但一个页面中,N件事情的代码在一个组件内互相纠缠确实是在Hooks出现之前非常常见的一种状态。

那么Hooks写法在代码组织上究竟能带来怎样的提升呢?

(假设上图中每一种颜色就代码一种高度相关的业务逻辑)

无论是vue还是react,通过Hooks写法都能做到,将“分散在各种声明周期里的代码块”,通过Hooks的方式将相关的内容聚合到一起。

这样带来的好处是显而易见的:“高度聚合,可阅读性提升”。伴随而来的便是“效率提升,bug变少”

按照“物理学”里的理论来说,这种代码组织方式,就算是“熵减”了。

3.3比class组件更容易理解

尤其是this

reactclass写法中,随处可见各种各样的.bind(this)。(甚至官方文档里也有专门的章节描述了“为什么绑定是必要的?”这一问题)

vue玩家别笑,computed:{a:()=>{this}}里的this也是undefined

很显然,绑定虽然“必要”,但并不是“优点”,反而是“故障高发”地段。

但在Hooks写法中,你就完全不必担心this的问题了。

因为:

本来无一物,何处惹尘埃。

Hooks写法直接告别了this,从“函数”来,到“函数”去。

妈妈再也不用担心我忘记写bind了。

3.4友好的渐进式

随风潜入夜,润物细无声。

渐进式的含义是:你可以一点点深入使用。

无论是vue还是react,都只是提供了HooksAPI,并将它们的优劣利弊摆在了那里。并没有通过无法接受的breakchange来强迫你必须使用Hooks去改写之前的class组件。

你依然可以在项目里一边写class组件,一边写Hooks组件,在项目的演进和开发过程中,这是一件没有痛感,却悄无声息改变着一切的事情。

但是事情发展的趋势却很明显,越来越多的人加入了Hooks组合式API的大军。

四、如何开始玩hooks

4.1环境和版本

react项目中,react的版本需要高于16.8.0

而在vue项目中,vue3.x是最好的选择,但vue2.6配合@vue/composition-api,也可以开始享受“组合式API”的快乐。

4.2react的Hooks写法

因为reactHooks仅支持“函数式”组件,因此需要创建一个函数式组件my-component.js

//my-component.jsimport{useState,useEffect}from'React'exportdefault()=>{//通过useState可以创建一个状态属性和一个赋值方法const[name,setName]=useState('')//通过useEffect可以对副作用进行处理useEffect(()=>{console.log(name)},[name])//通过useMemo能生成一个依赖name的变量messageconstmessage=useMemo(()=>{return`hello,mynameis${name}`},[name])return<div>{message}</div>}

细节可参考react官方网站:https://react.docschina.org/docs/hooks-intro.html

4.3vue的Hooks写法

vue的Hooks写法依赖于组合式API,因此本例采用<scriptsetup>来写:

<template><div>{{message}}</div></template><scriptsetup>import{computed,ref}from'vue'//定义了一个ref对象constname=ref('')//定义了一个依赖name.value的计算属性constmessage=computed(()=>{return`hello,mynameis${name.value}`})</script>

很明显,vue组合式API里完成useStateuseMemo相关工作的API并没有通过useXxx来命名,而是遵从了Vue一脉相承而来的refcomputed

虽然不符合reactHook定义的Hook约定,但vueapi不按照react的约定好像也并没有什么不妥。

参考网址:https://v3.cn.vuejs.org/api/composition-api.html

五、开始第一个自定义hook

除了官方提供的HooksApi,Hooks的另外一个重要特质,就是可以自己进行“自定义Hooks”的定义,从而完成状态逻辑的复用。

开源社区也都有很多不错的基于Hooks的封装,比如ahooks(ahooks.js.org/zh-CN/),又比如vueuse(vueuse.org/)

我还专门写过一篇小文章介绍vuehook:

  • 【一库】vueuse:我不许身为vuer,你的工具集只有lodash!。

  • https://www.swvq.com/post/7030395303433863205

那么,我们应该怎么开始撰写“自定义Hooks”呢?往下看吧!

5.1react玩家看这里

react官方网站就专门有一个章节讲述“自定义Hook”。(https://react.docschina.org/docs/hooks-custom.html)

这里,我们扔用文章开头那个useName的需求为例,希望达到效果:

const{name,setName}=useName();//随机生成一个状态属性name,它有一个随机名作为初始值//并且提供了一个可随时更新该值的方法setName

如果我们要实现上面效果,我们该怎么写代码呢?

importReactfrom'react';exportconstuseName=()=>{//这个useMemo很关键constrandomName=React.useMemo(()=>genRandomName(),[]);const[name,setName]=React.useState(randomName)return{name,setName}}

忍不住要再次感叹一次,和mixins相比,它不仅使用起来更棒,就连定义起来也那么简单。

可能有朋友会好奇,为什么不直接这样写:

const[name,setName]=React.useState(genRandomName())

因为这样写是不对的,每次使用该Hook的函数组件被渲染一次时,genRandom()方法就会被执行一次,虽然不影响name的值,但存在性能消耗,甚至产生其他bug

为此,我写了一个能复现错误的demo,有兴趣的朋友可以点开验证:https://codesandbox.io/s/long-cherry-kzcbqr

2022-02-03日补充更正:经掘友提醒,可以通过React.useState(()=>randomName())传参来避免重复执行,这样就不需要useMemo了,感谢!

5.2vue玩家看这里

vue3官网没有关于自定义Hook的玩法介绍,但实践起来也并不困难。

目标也定位实现一个useName方法:

import{ref}from'vue';exportconstuseName=()=>{constname=ref(genRandomName())constsetName=(v)=>{name.value=v}return{name,setName}}

5.3vuereact自定义Hook的异同

  • 相似点:总体思路是一致的都遵照着"定义状态数据","操作状态数据","隐藏细节"作为核心思路。

  • 差异点:组合式APIReact函数组件有着本质差异
    vue3的组件里,setup是作为一个早于“created”的生命周期存在的,无论如何,在一个组件的渲染过程中只会进入一次。
    React函数组件则完全不同,如果没有被memorized,它们可能会被不停地触发,不停地进入并执行方法,因此需要开销的心智相比于vue其实是更多的。

作者:春哥的梦想是摸鱼

发表评论

  • 评论列表
还没有人评论,快来抢沙发吧~