BACK
Featured image of post 淺談為什麼 Vue 和 React 都選擇了 Hooks?

淺談為什麼 Vue 和 React 都選擇了 Hooks?

使用 Hook,你可以從 component 抽取 stateful 的邏輯,如此一來它就可以獨立地被測試和重複使用。Hook 讓你不需要改變 component 階層就能重用 stateful 的邏輯。這讓在許多 component 之間共用或是與社群共用 Hook 很簡單。

參考網站


一、hooks:什麼叫大勢所趨?

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

2019年6月,尤雨溪在 vue/github-issues 裡提出了關於 vue3 Composition API 的提案。(vue hooks的基礎)

在後續的 reactvue3 相關版本中,相關 hooks 能力都開始被更多人所接受。

除此之外,solid.jspreact 等框架,也是開始選擇加入 hooks 大家庭。

可以預見,雖然目前仍然是 class Component 和 hooks api 並駕齊驅的場面,但未來幾年裡,hooks 極有可能取代 class Component 成為業內真正的主流。


二、什麼是 hooks?

hooks 的定義

hooks 直譯是"鉤子",它並不僅是 react,甚至不僅是前端界的專用術語,而是整個行業所熟知的用語。

通常指:

系統運行到某一時期時,會調用被註冊到該時機的回調函數。

比較常見的鉤子有:windows 系統的鉤子能監聽到系統的各種事件,瀏覽器提供的 onloadaddEventListener 能註冊在瀏覽器各種時機被調用的方法。

以上這些,都可以被稱一聲 hook

但是很顯然,在特定領域的特定話題下,hooks 這個詞被賦予了一些特殊的含義。

[email protected] 之前,當我們談論 hooks 時,我們可能談論的是"組件的生命週期"。

但是現在,hooks 則有了全新的含義。

react 為例,hooks 是:

一系列以 “use” 作為開頭的方法,它們提供了讓你可以完全避開 class 式寫法,在函數式組件中完成生命週期、狀態管理、邏輯複用等幾乎全部組件開發工作的能力。

簡化一下:

一系列方法,提供了在函數式組件中完成開發工作的能力。

(記住這個關鍵詞:函數式組件)

1
2
import { useState, useEffect, useCallback } from 'react';
// 比如以上這幾個方法,就是最為典型的 Hooks

而在 vue 中,hooks 的定義可能更模糊,姑且總結一下:

vue 組合式 API 裡,以 “use” 作為開頭的,一系列提供了組件複用狀態管理…等開發能力的方法。

(關鍵詞:組合式API)

1
2
3
import { useSlots, useAttrs } from 'vue';
import { useRouter } from 'vue-router';
// 以上這些方法,也是 vue3 中相關的 Hook!

如:useSlotsuseAttrsuseRouter 等。

但主觀來說,我認為 vue 組合式 API 其本身就是 “vue hooks” 的關鍵一環,起到了 react hooks 裡對生命週期狀態管理的核心作用。(如 onMountedref 等等)。

如果按這個標準來看的話,vuereacthooks 的定義,似乎都差不多。

那麼為什麼要提到是以 "use" 作為開頭的方法呢?

命名規範和指導思想

通常來說,hooks 的命名都是以 use 作為開頭,這個規範也包括了那麼我們自定義的 hooks

為什麼?

react 官方文檔裡,對 hooks 的定義和使用提出了 一個假設、兩個只在 核心指導思想。

一個假設:假設任何以「use」 開頭並緊跟著一個大寫字母的函數就是一個 Hook

第一個只在:只在 React 函數組件中調用 Hook,而不在普通函數中調用 Hook。(Eslint 通過判斷一個方法是不是大坨峰命名來判斷它是否是 React 函數)

第二個只在:只在最頂層使用 Hook,而不要在循環、條件或嵌套函數中調用 Hook

因為約定的力量在於:我們不用細看實現,也能通過命名來了解一個它是什麼。

以上 一個假設、兩個只在 總結自 react 官網:


三、為什麼我們需要 hooks?

更好的狀態複用

針對的就是你,mixin!

class 組件模式下,狀態邏輯的複用是一件困難的事情。

假設有如下需求:

當組件實例創建時,需要創建一個 state 屬性:name,並隨機給此 name 屬性附一個初始值。除此之外,還得提供一個 setName 方法。你可以在組件其他地方取得和修改此狀態屬性。
更重要的是:這個邏輯要可以複用,在各種業務組件裡複用這個邏輯。

在擁有 Hooks 之前,我首先會想到的解決方案一定是 mixin

代碼如下:(此示例採用 vue2 mixin 寫法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 混入文件:name-mixin.js
export default {
  data() {
    return {
      name: genRandomName() // 假裝它能生成隨機的名字
    }
  },
  methods: {
    setName(name) {
      this.name = name
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- 组件my-component.vue -->
<template>
  <div>{{ name }}</div>
</template>

<script>
import nameMixin from './name-mixin';
export default {
  mixins: [nameMixin],
  // 通過 mixins,你可以直接獲得 nameMixin 中所定義的狀態、方法、生命周期中的事件...等
  mounted() {
    setTimeout(() => {
      this.setName('Tom')
    }, 3000)
  }
}
</script>

粗略看來,mixins 似乎提供了非常不錯的複用能力,但是,react 官方文檔直接表明:

為什麼呢?

因為 mixins 雖然提供了這種狀態複用的能力,但它的弊端實在太多了。

弊端一:難以追溯的方法與屬性!

試想一下,如果出現這種代碼,你是否會懷疑人生:

1
2
3
4
5
6
7
export default {
  mixins: [ a, b, c, d, e, f, g ], // 當然,這只是表示它混入了很多能力
  mounted() {
    console.log(this.name)
    // 嗯...這個 this.name 來自於誰?我難道要一個個混入看實現?
  }
}

又或者:

1
2
3
4
5
6
7
8
a.js mixins: [b.js]

b.js mixins: [c.js]

c.js mixins: [d.js]

// 你猜猜看,this.name 來自於誰?
// 求求你别再說了,我血壓已经上来了

弊端二:覆蓋、同名?貴圈真亂!

當我同時想混入 mixin-a.jsmixin-b.js 以同時獲得它們能力的時候,不幸的事情發生了:

由於這兩個 mixin 功能的開發者惺惺相惜,它們都定義了 this.name 作為屬性。

這種時候,你會深深懷疑,mixins 究竟是不是一種科學的複用方式。

弊端三:梅開二度?代價很大!

仍然說上面的例子,如果我的需求發生了改變,我需要的不再是一個簡單的狀態 name,而是分別需要 firstNamelastName

此時 name-mixin.js 混入的能力就會非常尷尬,因為我無法兩次 mixins 同一個文件。

當然,也是有解決方案的,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 動態生成 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'),
  ]
}

確實通過動態生成 mixin 完成了能力的複用,但這樣一來,無疑更加地增大了程序的複雜性,降低了可讀性。

因此一種新的狀態邏輯複用,就變得極為迫切了——————它就是 Hooks

Hook 的狀態複用寫法:

1
2
3
4
5
6
7
// 單個 name 的寫法
const { name, setName } = useName();

// 梅開二度的寫法
const { name: firstName, setName: setFirstName } = useName();

const { name: secondName, setName: setSecondName } = useName();

相比於 mixins,它們簡直太棒了!

  1. 方法和屬性好追溯嗎?這可太好了,誰產生的,哪兒來的一目了然。
  2. 會有重名、覆蓋問題嗎?完全沒有!內部的變量在閉包內,返回的變量支持定義別名。
  3. 多次使用,沒開 N 度?你看上面的代碼塊內不就 “梅開三度” 了嗎?

就衝 狀態邏輯復用 這個理由,Hooks 就已經香得我口水直流了。

代碼組織

熵減,宇宙哲學到編碼哲學。

項目、模塊、頁面、功能,如何高效而清晰地組織代碼,這一個看似簡單的命題就算寫幾本書也無法完全說清楚。

但一個頁面中,N 件事情的代碼在一個組件內互相糾纏確實是在 Hooks 出現之前非常常見的一種狀態。

那麼 Hooks 寫法在代碼組織上究竟能帶來怎樣的提升呢?

(假設上圖中每一種顏色就代碼一種高度相關的業務邏輯)

無論是 vue 還是 react,通過 Hooks 寫法都能做到,將"分散在各種聲明周期裡的代碼塊",通過 Hooks 的方式將相關的內容聚合到一起。

這樣帶來的好處是顯而易見的:高度聚合,可閱讀性提升。伴隨而來的便是效率提升,bug變少

按照"物理學"裡的理論來說,這種代碼組織方式,就算是"熵減"了。

比 class 組件更容易理解

尤其是 this

reactclass 寫法中,隨處可見各種各樣的 .bind(this)。(甚至官方文檔裡也有專門的章節描述了"為什麼綁定是必要的?“這一問題)

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

很顯然,綁定雖然"必要”,但並不是"優點",反而是"故障高發"地段。

但在 Hooks 寫法中,你就完全不必擔心 this 的問題了。

因為:

本來無一物,何處惹塵埃。

Hooks 寫法直接告別了 this,從"函數"來,到"函數"去。

媽媽再也不用擔心我忘記寫 bind 了。

友好的漸進式

隨風潛入夜,潤物細無聲。

漸進式的含義是:你可以一點點深入使用。

無論是 vue 還是 react,都只是提供了 Hooks API,並將它們的優劣利弊擺在了那裡。並沒有通過無法接受的 break change 來強迫你必須使用 Hooks 去改寫之前的 class 組件。

你依然可以在項目裡一邊寫 class 組件,一邊寫 Hooks 組件,在項目的演進和開發過程中,這是一件沒有痛感,卻悄無聲息改變著一切的事情。

但是事情發展的趨勢卻很明顯,越來越多的人加入了 Hooks组合式 API 的大軍。


如何開始玩 hooks?

環境和版本

react 項目中,react 的版本需要高於 16.8.0

而在 vue 項目中,vue3.x 是最好的選擇,但 vue2.6+ 配合 @vue/composition-api,也可以開始享受 “組合式 API” 的快樂。

react 的 Hooks 寫法

因為 react Hooks 僅支持 “函數式” 組件,因此需要創建一個函數式組件 my-component.js

my-component.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { useState, useEffect, useMemo } from 'React'

export default () => {
  // 通過 useState 可以創建一個 狀態屬性 和一個 賦值方法
  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>
}

細節可參考 react 官方網站

vue 的 Hooks 寫法

vueHooks 寫法依賴於组合式API,因此本例採用 <script setup> 來寫:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<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 裡完成 useStateuseMemo 相關工作的 API 並沒有通過 useXxx 來命名,而是遵從了 Vue 一脈相承而來的 refcomputed

雖然不符合 react Hook 定義的 Hook 約定,但 vueapi 不按照 react 的約定好像也並沒有什麼不妥。

參考網址


五、開始第一個自定義 hook

除了官方提供的 Hooks ApiHooks 的另外一個重要特質,就是可以自己進行 “自定義 Hooks” 的定義,從而完成狀態邏輯的複用。

開源社區也都有很多不錯的基於 Hooks 的封裝,比如 ahooks,又比如 VueUse

那麼,我們應該怎麼開始撰寫 “自定義 Hooks” 呢?往下看吧!

react 玩家看這裡

react 官方網站就專門有一個章節講述 “自定義 Hook"。

這裡,我們扔用文章開頭那個 useName 的需求為例,希望達到效果:

1
2
3
const { name, setName } = useName();
// 隨機生成一個狀態屬性 name,它有一個隨機名作為初始值
// 並且提供了一個可隨時更新該值的方法 setName

如果我們要實現上面效果,我們該怎麼寫代碼呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import React from 'react';

export const useName = () => {
  // 這個 useMemo 很關鍵
  const randomName = React.useMemo(() => genRandomName(), []);
  const [ name, setName ] = React.useState(randomName)

  return {
    name,
    setName
  }
}

忍不住要再次感嘆一次,和 mixins 相比,它不僅使用起來更棒,就連定義起來也那麼簡單。

可能有朋友會好奇,為什麼不直接這樣寫:

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

因為這樣寫是不對的,每次使用該 Hook 的函數組件被渲染一次時,genRandom() 方法就會被執行一次,雖然不影響 name 的值,但存在性能消耗,甚至產生其他 bug。

為此,我寫了一個能複現錯誤的 demo,有興趣的朋友可以驗證:

可以通過 React.useState(() => randomName()) 傳參來避免重複執行,這樣就不需要 useMemo 了!

vue 玩家看這裡

vue3 官網 沒有關於 自定義 Hook 的玩法介紹,但實踐起來也並不困難。

目標也定位實現一個 useName 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { ref } from 'vue';

export const useName = () => {
  const name = ref(genRandomName())
  const setName = (v) => {
    name.value = v
  }
  return {
    name,
    setName
  }
}

vue 和 react 自定義 Hook 的差異

  • 相似點:總體思路是一致的都遵照著 定義狀態數據操作狀態數據隱藏細節 作為核心思路。
  • 差異點:组合式 API 和 React 函数组件,有著本質差異:
    • Vue3 的組件裡,setup 是作為一個早於 “created” 的生命週期存在的,無論如何,在一個組件的渲染過程中只會進入一次。
    • React 函数组件則完全不同,如果沒有被 memorized,它們可能會被不停地觸發,不停地進入並執行方法,因此需要開銷的心智相比於 Vue 其實是更多的。

comments powered by Disqus