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