接下來我們來一起了解。
- 初步了解 Hooks 在 vue 與 react 的現(xiàn)狀
- 聽一聽本文作者關(guān)于 Hooks 的定義和總結(jié)
- 弄懂“為什么我們需要 Hooks”
- 進(jìn)行一些簡(jiǎn)單的 Hooks 實(shí)踐
一、hooks: 什么叫大勢(shì)所趨?
2019年年初,react 在 16.8.x 版本正式具備了 hooks 能力。
2019年6月,尤雨溪在 vue/github-issues 里提出了關(guān)于 vue3 Component API 的提案。(vue hooks的基礎(chǔ))
在后續(xù)的 react 和 vue3 相關(guān)版本中,相關(guān) hooks 能力都開始被更多人所接受。
除此之外,solid.js、 preact 等框架,也是開始選擇加入 hooks 大家庭。
可以預(yù)見,雖然目前仍然是 class Component 和 hooks api 并駕齊驅(qū)的場(chǎng)面,但未來幾年里,hooks 極有可能取代 class Component 成為業(yè)內(nèi)真正的主流。
二、什么是 hooks?
年輕時(shí)你不懂我,就像后來我不懂 hooks。
2.1 hooks 的定義
"hooks" 直譯是 “鉤子”,它并不僅是 react,甚至不僅是前端界的專用術(shù)語,而是整個(gè)行業(yè)所熟知的用語。通常指:
系統(tǒng)運(yùn)行到某一時(shí)期時(shí),會(huì)調(diào)用被注冊(cè)到該時(shí)機(jī)的回調(diào)函數(shù)。
比較常見的鉤子有:windows 系統(tǒng)的鉤子能監(jiān)聽到系統(tǒng)的各種事件,瀏覽器提供的 onload 或 addEventListener 能注冊(cè)在瀏覽器各種時(shí)機(jī)被調(diào)用的方法。
以上這些,都可以被稱一聲 "hook"。
但是很顯然,在特定領(lǐng)域的特定話題下,hooks 這個(gè)詞被賦予了一些特殊的含義。
在 [email protected] 之前,當(dāng)我們談?wù)?nbsp;hooks 時(shí),我們可能談?wù)摰氖?ldquo;組件的生命周期”。
但是現(xiàn)在,hooks 則有了全新的含義。
以 react 為例,hooks 是:
一系列以 “use” 作為開頭的方法,它們提供了讓你可以完全避開 class式寫法,在函數(shù)式組件中完成生命周期、狀態(tài)管理、邏輯復(fù)用等幾乎全部組件開發(fā)工作的能力。
簡(jiǎn)化一下:
一系列方法,提供了在函數(shù)式組件中完成開發(fā)工作的能力。
(記住這個(gè)關(guān)鍵詞: 函數(shù)式組件)
import { useState, useEffect, useCallback } from 'react';
// 比如以上這幾個(gè)方法,就是最為典型的 Hooks
而在 vue 中, hooks 的定義可能更模糊,姑且總結(jié)一下:
在 vue 組合式API里,以 “use” 作為開頭的,一系列提供了組件復(fù)用、狀態(tài)管理等開發(fā)能力的方法。
(關(guān)鍵詞:組合式API)
import { useSlots, useAttrs } from 'vue';
import { useRouter } from 'vue-router';
// 以上這些方法,也是 vue3 中相關(guān)的 Hook!
如:useSlots、 useAttrs、 useRouter 等。
但主觀來說,我認(rèn)為vue 組合式API其本身就是“vue hooks”的關(guān)鍵一環(huán),起到了 react hooks里對(duì)生命周期、狀態(tài)管理的核心作用。(如 onMounted、 ref 等等)。
如果按這個(gè)標(biāo)準(zhǔn)來看的話,vue 和 react 中 hooks 的定義,似乎都差不多。
那么為什么要提到是以 “use” 作為開頭的方法呢?
2.2 命名規(guī)范 和 指導(dǎo)思想
通常來說,hooks 的命名都是以 use 作為開頭,這個(gè)規(guī)范也包括了那么我們自定義的 hooks。
為什么?
因?yàn)?nbsp;(愛情 誤) 約定。
在 react 官方文檔里,對(duì) hooks 的定義和使用提出了 “一個(gè)假設(shè)、兩個(gè)只在” 核心指導(dǎo)思想。(播音腔)
一個(gè)假設(shè): 假設(shè)任何以 「use」 開頭并緊跟著一個(gè)大寫字母的函數(shù)就是一個(gè) Hook。
第一個(gè)只在: 只在 React 函數(shù)組件中調(diào)用 Hook,而不在普通函數(shù)中調(diào)用 Hook。(Eslint 通過判斷一個(gè)方法是不是大坨峰命名來判斷它是否是 React 函數(shù))
第二個(gè)只在: 只在最頂層使用 Hook,而不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook。
因?yàn)槭羌s定,因而 useXxx 的命名并非強(qiáng)制,你依然可以將你自定義的 hook 命名為 byXxx 或其他方式,但不推薦。
因?yàn)榧s定的力量在于:我們不用細(xì)看實(shí)現(xiàn),也能通過命名來了解一個(gè)它是什么。
以上 “一個(gè)假設(shè)、兩個(gè)只在” 總結(jié)自 react 官網(wǎng):
- 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 更好的狀態(tài)復(fù)用
懟的就是你,mixin !
在 class 組件模式下,狀態(tài)邏輯的復(fù)用是一件困難的事情。
假設(shè)有如下需求:
當(dāng)組件實(shí)例創(chuàng)建時(shí),需要?jiǎng)?chuàng)建一個(gè) state 屬性:name,并隨機(jī)給此 name 屬性附一個(gè)初始值。除此之外,還得提供一個(gè) setName 方法。你可以在組件其他地方開銷和修改此狀態(tài)屬性。
更重要的是: 這個(gè)邏輯要可以復(fù)用,在各種業(yè)務(wù)組件里復(fù)用這個(gè)邏輯。
在擁有 Hooks 之前,我首先會(huì)想到的解決方案一定是 mixin。
代碼如下:(此示例采用 vue2 mixin 寫法 )
// 混入文件:name-mixin.js
export default {
data() {
return {
name: genRandomName() // 假裝它能生成隨機(jī)的名字
}
},
methods: {
setName(name) {
this.name = name
}
}
}
// 組件:my-component.vue
<template>
<div>{{ name }}</div>
<template>
<script>
import nameMixin from './name-mixin';
export default {
mixins: [nameMixin],
// 通過mixins, 你可以直接獲得 nameMixin 中所定義的狀態(tài)、方法、生命周期中的事件等
mounted() {
setTimeout(() => {
this.setName('Tom')
}, 3000)
}
}
<script>
粗略看來,mixins 似乎提供了非常不錯(cuò)的復(fù)用能力,但是,react官方文檔直接表明:
為什么呢?
因?yàn)?nbsp;mixins 雖然提供了這種狀態(tài)復(fù)用的能力,但它的弊端實(shí)在太多了。
弊端一:難以追溯的方法與屬性!
試想一下,如果出現(xiàn)這種代碼,你是否會(huì)懷疑人生:
export default {
mixins: [ a, b, c, d, e, f, g ], // 當(dāng)然,這只是表示它混入了很多能力
mounted() {
console.log(this.name)
// mmp!這個(gè) this.name 來自于誰?我難道要一個(gè)個(gè)混入看實(shí)現(xiàn)?
}
}
又或者:
a.js mixins: [b.js]
b.js mixins: [c.js]
c.js mixins: [d.js]
// 你猜猜看, this.name 來自于誰?
// 求求你別再說了,我血壓已經(jīng)上來了
弊端二:覆蓋、同名?貴圈真亂!
當(dāng)我同時(shí)想混入 mixin-a.js 和 mixin-b.js 以同時(shí)獲得它們能力的時(shí)候,不幸的事情發(fā)生了:
由于這兩個(gè) mixin 功能的開發(fā)者惺惺相惜,它們都定義了 this.name 作為屬性。
這種時(shí)候,你會(huì)深深懷疑,mixins 究竟是不是一種科學(xué)的復(fù)用方式。
弊端三:梅開二度?代價(jià)很大!
仍然說上面的例子,如果我的需求發(fā)生了改變,我需要的不再是一個(gè)簡(jiǎn)單的狀態(tài) name,而是分別需要 firstName 和 lastName。此時(shí) name-mixin.js 混入的能力就會(huì)非常尷尬,因?yàn)槲覠o法兩次 mixins 同一個(gè)文件。
當(dāng)然,也是有解決方案的,如:
// 動(dòng)態(tài)生成mixin
function genNameMixin(key, funcKey) {
return {
data() {
return {
[key]: genRandomName()
}
},
methods: {
[funcKey]: function(v) {
this.[key] = v
}
}
}
}
export default {
mixins: [
genNameMixin('firstName', 'setFirstName'),
genNameMixin('lastName', 'setLastName'),
]
}
確實(shí)通過動(dòng)態(tài)生成 mixin 完成了能力的復(fù)用,但這樣一來,無疑更加地增大了程序的復(fù)雜性,降低了可讀性。
因此,一種新的 “狀態(tài)邏輯復(fù)用” 就變得極為迫切了——它就是 Hooks!
Hook 的狀態(tài)復(fù)用寫法:
// 單個(gè)name的寫法
const { name, setName } = useName();
// 梅開二度的寫法
const { name : firstName, setName : setFirstName } = useName();
const { name : secondName, setName : setSecondName } = useName();
相比于 mixins,它們簡(jiǎn)直太棒了!
- 方法和屬性好追溯嗎?這可太好了,誰產(chǎn)生的,哪兒來的一目了然。
- 會(huì)有重名、覆蓋問題嗎?完全沒有!內(nèi)部的變量在閉包內(nèi),返回的變量支持定義別名。
- 多次使用,沒開N度?你看上面的代碼塊內(nèi)不就“梅開三度” 了嗎?
就沖 “狀態(tài)邏輯復(fù)用” 這個(gè)理由,Hooks 就已經(jīng)香得我口水直流了。
3.2 代碼組織
熵減,宇宙哲學(xué)到編碼哲學(xué)。
項(xiàng)目、模塊、頁(yè)面、功能,如何高效而清晰地組織代碼,這一個(gè)看似簡(jiǎn)單的命題就算寫幾本書也無法完全說清楚。
但一個(gè)頁(yè)面中,N件事情的代碼在一個(gè)組件內(nèi)互相糾纏確實(shí)是在 Hooks 出現(xiàn)之前非常常見的一種狀態(tài)。
那么 Hooks 寫法在代碼組織上究竟能帶來怎樣的提升呢?
(假設(shè)上圖中每一種顏色就代碼一種高度相關(guān)的業(yè)務(wù)邏輯)
無論是 vue 還是 react, 通過 Hooks 寫法都能做到,將“分散在各種聲明周期里的代碼塊”,通過 Hooks 的方式將相關(guān)的內(nèi)容聚合到一起。
這樣帶來的好處是顯而易見的:“高度聚合,可閱讀性提升”。伴隨而來的便是 “效率提升,bug變少”。
按照“物理學(xué)”里的理論來說,這種代碼組織方式,就算是“熵減”了。
3.3 比 class 組件更容易理解
尤其是 this 。
在 react 的 class 寫法中,隨處可見各種各樣的 .bind(this)。(甚至官方文檔里也有專門的章節(jié)描述了“為什么綁定是必要的?”這一問題)
vue 玩家別笑,computed: { a: () => { this } } 里的 this 也是 undefined。
很顯然,綁定雖然“必要”,但并不是“優(yōu)點(diǎn)”,反而是“故障高發(fā)”地段。
但在Hooks 寫法中,你就完全不必?fù)?dān)心 this 的問題了。
因?yàn)椋?/p>
本來無一物,何處惹塵埃。
Hooks 寫法直接告別了 this,從“函數(shù)”來,到“函數(shù)”去。
媽媽再也不用擔(dān)心我忘記寫 bind 了。
3.4 友好的漸進(jìn)式
隨風(fēng)潛入夜,潤(rùn)物細(xì)無聲。
漸進(jìn)式的含義是:你可以一點(diǎn)點(diǎn)深入使用。
無論是 vue 還是 react,都只是提供了 Hooks API,并將它們的優(yōu)劣利弊擺在了那里。并沒有通過無法接受的 break change 來強(qiáng)迫你必須使用 Hooks 去改寫之前的 class 組件。
你依然可以在項(xiàng)目里一邊寫 class 組件,一邊寫 Hooks 組件,在項(xiàng)目的演進(jìn)和開發(fā)過程中,這是一件沒有痛感,卻悄無聲息改變著一切的事情。
但是事情發(fā)展的趨勢(shì)卻很明顯,越來越多的人加入了 Hooks 和 組合式API 的大軍。
四、如何開始玩 hooks ?
4.1 環(huán)境和版本
在 react 項(xiàng)目中, react 的版本需要高于 16.8.0。
而在 vue 項(xiàng)目中, vue3.x 是最好的選擇,但 vue2.6+ 配合 @vue/composition-api,也可以開始享受“組合式API”的快樂。
4.2 react 的 Hooks 寫法
因?yàn)?react Hooks 僅支持“函數(shù)式”組件,因此需要?jiǎng)?chuàng)建一個(gè)函數(shù)式組件 my-component.js。
// my-component.js
import { useState, useEffect } from 'React'
export default () => {
// 通過 useState 可以創(chuàng)建一個(gè) 狀態(tài)屬性 和一個(gè)賦值方法
const [ name, setName ] = useState('')
// 通過 useEffect 可以對(duì)副作用進(jìn)行處理
useEffect(() => {
console.log(name)
}, [ name ])
// 通過 useMemo 能生成一個(gè)依賴 name 的變量 message
const message = useMemo(() => {
return `hello, my name is ${name}`
}, [name])
return <div>{ message }</div>
}
細(xì)節(jié)可參考 react 官方網(wǎng)站:
https://react.docschina.org/docs/hooks-intro.html
4.3 vue 的 Hooks 寫法
vue 的 Hooks 寫法依賴于 組合式API,因此本例采用 <script setup> 來寫:
<template>
<div>
{{ message }}
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
// 定義了一個(gè) ref 對(duì)象
const name = ref('')
// 定義了一個(gè)依賴 name.value 的計(jì)算屬性
const message = computed(() => {
return `hello, my name is ${name.value}`
})
</script>
很明顯,vue 組合式API里完成 useState 和 useMemo 相關(guān)工作的 API 并沒有通過 useXxx 來命名,而是遵從了 Vue 一脈相承而來的 ref 和 computed。
雖然不符合 react Hook 定義的 Hook 約定,但 vue 的 api 不按照 react 的約定好像也并沒有什么不妥。
參考網(wǎng)址:
https://v3.cn.vuejs.org/api/composition-api.html
五、開始第一個(gè)自定義 hook
除了官方提供的 Hooks Api, Hooks 的另外一個(gè)重要特質(zhì),就是可以自己進(jìn)行“自定義 Hooks” 的定義,從而完成狀態(tài)邏輯的復(fù)用。
開源社區(qū)也都有很多不錯(cuò)的基于 Hooks 的封裝,比如 ahooks (
https://ahooks.js.org/zh-CN/),又比如 vueuse(https://vueuse.org/)
我還專門寫過一篇小文章介紹 vuehook:【一庫(kù)】vueuse:我不許身為vuer,你的工具集只有l(wèi)odash!。
那么,我們應(yīng)該怎么開始撰寫 “自定義Hooks” 呢?往下看吧!
5.1 react 玩家看這里
react 官方網(wǎng)站就專門有一個(gè)章節(jié)講述“自定義Hook”。(
https://react.docschina.org/docs/hooks-custom.html)
這里,我們?nèi)佑梦恼麻_頭那個(gè) useName 的需求為例,希望達(dá)到效果:
const { name, setName } = useName();
// 隨機(jī)生成一個(gè)狀態(tài)屬性 name,它有一個(gè)隨機(jī)名作為初始值
// 并且提供了一個(gè)可隨時(shí)更新該值的方法 setName
如果我們要實(shí)現(xiàn)上面效果,我們?cè)撛趺磳懘a呢?
import React from 'react';
export const useName = () => {
// 這個(gè) useMemo 很關(guān)鍵
const randomName = React.useMemo(() => genRandomName(), []);
const [ name, setName ] = React.useState(randomName)
return {
name,
setName
}
}
忍不住要再次感嘆一次,和 mixins 相比,它不僅使用起來更棒,就連定義起來也那么簡(jiǎn)單。
可能有朋友會(huì)好奇,為什么不直接這樣寫:
const [ name, setName ] = React.useState(genRandomName())
因?yàn)檫@樣寫是不對(duì)的,每次使用該 Hook 的函數(shù)組件被渲染一次時(shí),genRandom() 方法就會(huì)被執(zhí)行一次,雖然不影響 name 的值,但存在性能消耗,甚至產(chǎn)生其他 bug。
為此,我寫了一個(gè)能復(fù)現(xiàn)錯(cuò)誤的demo,有興趣的朋友可以點(diǎn)開驗(yàn)證:
https://codesandbox.io/s/long-cherry-kzcbqr
2022-02-03日補(bǔ)充更正:經(jīng)掘友提醒,可以通過 React.useState(() => randomName()) 傳參來避免重復(fù)執(zhí)行,這樣就不需要 useMemo 了,感謝!
5.2 vue 玩家看這里
vue3 官網(wǎng)沒有關(guān)于 自定義Hook 的玩法介紹,但實(shí)踐起來也并不困難。
目標(biāo)也定位實(shí)現(xiàn)一個(gè) useName 方法:
import { ref } from 'vue';
export const useName = () => {
const name = ref(genRandomName())
const setName = (v) => {
name.value = v
}
return {
name,
setName
}
}
5.3 vue 和 react 自定義 Hook 的異同
- 相似點(diǎn): 總體思路是一致的 都遵照著 "定義狀態(tài)數(shù)據(jù)","操作狀態(tài)數(shù)據(jù)","隱藏細(xì)節(jié)" 作為核心思路。
- 差異點(diǎn): 組合式API 和 React函數(shù)組件 有著本質(zhì)差異vue3 的組件里, setup 是作為一個(gè)早于 “created” 的生命周期存在的,無論如何,在一個(gè)組件的渲染過程中只會(huì)進(jìn)入一次。React函數(shù)組件 則完全不同,如果沒有被 memorized,它們可能會(huì)被不停地觸發(fā),不停地進(jìn)入并執(zhí)行方法,因此需要開銷的心智相比于vue其實(shí)是更多的。
本文轉(zhuǎn)載于掘金,謝謝,大家一起學(xué)習(xí)…!
…https://juejin.cn/post/7066951709678895141?share_token=fe63ed48-8486-473a-a1a2-88b14fe41e4a