9102,作為前端必須知道 hook 怎麼玩了

Guokai發表於2019-06-13

9102,作為前端必須知道 hook 怎麼玩了

背景

很榮幸在6月8號那天參加了在上海舉辦的vueconf,其中尤大本人講解的vue3.0的介紹中,見識到了vue3.0的一些新特性,其中最重要的一項RFC就是 Vue Function-based API RFC,很巧的在不久前正好研究了一下react hook,感覺2者的在思想上有著異曲同工之妙,所以有了一個想總結一下關於hook的想法,同時看到很多人關於hook的介紹都是分開講的,當然可能和vue3.0對於這個特性的說明剛剛問世也有一定的關係,so,lets begin~

什麼是hook

首先我們需要了解什麼是hook,拿react的介紹來看,它的定義是:

它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性

在16.8以前的版本中,我們在寫react元件的時候,大部分都都是class component,因為基於class的元件react提供了更多的可操作性,比如擁有自己的state,以及一些生命週期的實現,對於複雜的邏輯來講class的支援程度是更高的:

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() { // do sth... }

  componentWillUnmount() { // do sth... }
  
  // other methods or lifecycle...
  
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
複製程式碼

同時,對於function component來說,react也是支援的,但是function component只能擁有props,不能擁有state,也就是隻能實現stateless component:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
複製程式碼

react 並沒有提供在函式元件中設定state以及生命週期的一些操作方法,所以那個時候,極少的場景下適合採用函式元件,但是16.8版本出現hook以後情況得到了改變,hook的目標就是--讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性,來看個例子:

import React, { useState } from 'react';

function Example() {
  // 宣告一個新的叫做 “count” 的 state 變數
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼

useState就是react提供的一個Hook,通過它我們就可以在function元件中設定自己想要的state了,不僅可以使用還可以很方便的去通過setState(注意不是class中的setState,這裡指的是上述例子中的setCount)更改,當然,react提供了很多hook來支援不同的行為和操作,下面我們還會再簡單介紹,我們在看下vue hook,這是尤大在vueconf上分享的一段程式碼:

import { value, computed, watch, onMounted } from 'vue'

const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  setup() {
    // reactive state
    const count = value(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}
複製程式碼

從上面的例子中不難看出,和react hook的用法非常相似,並且尤大也有說這個RFC是借鑑了react hook的想法,但是規避了一些react的問題,然後這裡解釋一下為什麼我把vue的這個RFC也稱為是hook,因為在react hook的介紹中有這麼一句話,什麼是hook--Hook 是一些可以讓你在函式元件裡“鉤入” React state 及生命週期等特性的函式,那麼vue提供的這些API的作用也是類似的--可以讓你在函式元件裡“鉤入” value(2.x中的data) 及生命週期等特性的函式,所以,暫且就叫vue-hook吧~

hook的時代意義

那麼,hook的時代意義是什麼?我們從頭來說,框架是服務於業務的,業務中很難避免的一個問題就是-- 邏輯複用,同樣的功能,同樣的元件,在不一樣的場合下,我們有時候不得不去寫2+次,為了避免耦合,後來各大框架紛紛想出了一些辦法:

  • mixin
  • HOC
  • slot

各大框架的使用情況:

  • react 和 vue都曾用過mixin(react 目前已經廢棄),
  • Higher-Order-Components(HOC) react中用的相對多一點,vue的話,巢狀template有點。。彆扭,
  • slot vue中用的多一些,react基本不需要slot這種用法,

上述這些方法都可以實現邏輯上的複用,但是都有一些額外的問題:

  • mixin的問題:

    • 可能會相互依賴,相互耦合,不利於程式碼維護;
    • 不同的mixin中的方法可能會相互衝突;
    • mixin非常多時,元件是可以感知到的,甚至還要為其做相關處理, 這樣會給程式碼造成滾雪球式的複雜性
  • HOC的問題:

    • 需要在原元件上進行包裹或者巢狀,如果大量使用HOC, 將會產生非常多的巢狀,這讓除錯變得非常困難;
    • HOC可以劫持props,在不遵守約定的情況下也可能造成衝突
    • props 也可能造成命名的衝突
    • wrapper hell

有沒有見過這樣的dom結構?

9102,作為前端必須知道 hook 怎麼玩了

這就是wrapper hell的典型代表~

所以,hook的出現是劃時代的,它通過function抽離的方式,實現了複雜邏輯的內部封裝,根據上述我們提出的問題總結了hook的一些優點:

  1. 邏輯程式碼的複用
  2. 減小了程式碼體積
  3. 沒有this的煩惱

帶著這些思想,我們一起看下react和vue分別的實現:

react hook簡介

Dan 講解hook的視訊在這裡,如果你看不了這個,可以嘗試看官網介紹

我們用同樣功能的程式碼來看react hook,實現一個監聽滑鼠變化,並實時檢視位置的功能,同時我們把位置資訊掛到title上面,用class component我們要這樣寫:

import React, { Component } from 'react';

export default class MyClassApp extends Component {
    constructor(props) {
        super(props);
        this.state = {
            x: 0,
            y: 0
        };
        this.handleUpdate = this.handleUpdate.bind(this);
    }
    componentDidMount() {
        document.addEventListener('mousemove', this.handleUpdate);
    }
    componentDidUpdate() {
        const { x, y } = this.state;
        document.title = `(${x},${y})`;
    }
    componentWillUnmount() {
        window.removeEventListener('mousemove', this.handleUpdate);
    }
    handleUpdate(e) {
        this.setState({
            x: e.clientX,
            y: e.clientY
        });
    }
    render() {
        return (
            <div>
                current position x:{this.state.x}, y:{this.state.y}
            </div>
        );
    }
}

複製程式碼

線上程式碼演示在這裡

同樣的邏輯我們換用hook來實現

import React, { useState, useEffect } from 'react';

// 自定義hook useMousePostion
const useMousePostion = () => {
    // 使用hookuseState初始化一個state
    const [postion, setPostion] = useState({ x: 0, y: 0 });
    function handleMove(e) {
        setPostion({ x: e.clientX, y: e.clientY });
    }
    // 使用useEffect處理class中生命週期可以做到的事情
    // 注:效果一樣,但是實際的原理並不同,有興趣可以去官網仔細研究
    useEffect(() => {
        // 同時可以處理 componentDidMount 以及 componentDidUpdate 中的事情
        window.addEventListener('mousemove', handleMove);
        document.title = `(${postion.x},${postion.y})`;
        return () => {
            // return的function 可以相當於在元件被解除安裝的時候執行 類似於 componentWillUnmount
            window.removeEventListener('mousemove', handleMove);
        };
        // [] 是引數,代表deps,也就是說react觸發這個hook的時機會和傳入的deps有關,內部利用objectIs實現
        // 預設不給引數會在每次render的時候呼叫,給空陣列會導致每次比較是一樣的,只執行一次,這裡正確的應該是給postion
    }, [postion]);
    // postion 可以被直接return,這樣達到了邏輯的複用~,哪裡需要哪裡呼叫就可以了。
    return postion;
};

export default function App() {
    const { x, y } = useMousePostion(); // 內部維護自己的postion相關的邏輯
    return (
        <div>
            current position x: {x}, y: {y}
        </div>
    );
}

複製程式碼

線上程式碼演示在這裡

感謝評論區大佬的指正,查了react原始碼,這裡為了處理object.is的相容性,用了objectIs

可以看出用了hook之後,我們把關於position的邏輯都放到一個自定義的hook--useMousePostion 中,之後複用是很方便的,而且可以在內部進行維護postion獨有的邏輯而不影響外部內容,比起class元件,抽象能力更強。

當然,react hook 想要用好不可能這麼簡單,講解的文章也很多,這篇文章不深入太多,只是一個拋磚引玉,下面給大家安利幾個不錯的資源:

入門:

深入:

另外放一個筆者自己關於用hook實現redux的最佳實踐,注意是筆者自己這麼認為的,歡迎大佬們指出問題,參考的這個文章

vue hook簡介

尤大講解的視訊在這裡

程式碼因為vue3.0尚未釋出,我們還是看尤大給的demo程式碼:

import { value, computed, watch, onMounted } from 'vue'

function useMouse() {
  const x = value(0)
  const y = value(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

// 在元件中使用該函式
const Component = {
  setup() {
    const { x, y } = useMouse()
    // 與其它函式配合使用
    const { z } = useOtherLogic()
    return { x, y, z }
  },
  template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}
複製程式碼

可以看出來同樣我們可以抽離一些需要複用的邏輯到一個單獨的函式useMouse中,然後在這個函式裡面定義的一些生命週期和value的內容會隨著setup函式的呼叫被“鉤入(hook)”到元件上,並且這個函式return出來的資料可以直接被用在模板上,更具體的玩法我們坐等3.0的出現吧。

基礎內容不過多介紹,畢竟真實的api還沒釋出,想了解具體內容的可以來看尤大的講解

same & diff Point

看完了2個框架關於hook的實現,我們來做個簡單的對比

  1. Same Point:
  • 出現的背景,解決的問題是一樣的,2個框架都是為了解決邏輯複用過亂,程式碼體積過大等一些問題,包括this問題,使用function函式我們很少會和this去打交道了。
  • 使用方式類似,都是把可以複用的一些單獨的邏輯抽離到一個單獨的函式中去,同時返回元件中需要用到的資料,並且內部會自我維護資料的更新,從而觸發檢視的更新
  1. Diff Point:

實現原理不同 react hook底層是基於連結串列實現,呼叫的條件是每次元件被render的時候都會順序執行所有的hooks,所以下面的程式碼會報錯

function App(){
    const [name, setName] = useState('demo');
    if(condition){
        const [val, setVal] = useState('');
    }
}
複製程式碼

因為底層是連結串列,每一個hook的next是指向下一個hook的,if會導致順序不正確,從而導致報錯,所以react是不允許這樣使用hook的。

vue hook只會在setup函式被呼叫的時候被註冊一次,react資料更改的時候,會導致重新render,重新render又會重新把hooks重新註冊一次,所以react的上手難度更高一些,而vue之所以能避開這些麻煩的問題,根本原因在於它對資料的響應是基於proxy的,這種場景下,只要任何一個更改data的地方,相關的function或者template都會被重新計算,因此避開了react可能遇到的效能上的問題

當然react對這些都有解決方案,想了解的同學可以去看官網有介紹,比如useCallback,useMemo等hook的作用,我們看下尤大對vue和react hook的總結對比:

1.整體上更符合 JavaScript 的直覺;

2.不受呼叫順序的限制,可以有條件地被呼叫;

3.不會在後續更新時不斷產生大量的行內函數而影響引擎優化或是導致 GC 壓力;

4.不需要總是使用 useCallback 來快取傳給子元件的回撥以防止過度更新;

5.不需要擔心傳了錯誤的依賴陣列給 useEffect/useMemo/useCallback 從而導致回撥中使用了過期的值 —— Vue 的依賴追蹤是全自動的。

不得不說,青出於藍而勝於藍,vue雖然借鑑了react,但是天然的響應式資料,完美的避開了一些react hook遇到的短板~

總結

  1. function component 將會是接下來各大框架發展的一個方向,function天然對TS的友好也是一個重要的影響;
  2. react hook的上手成本相對於vue會難一些,vue天生規避了一些react中比較難處理的地方;
  3. hook一定是大前端的一個趨勢,現在才是剛剛開始的階段:SwiftUI-Hooks, flutter_hooks...

因為vue3.0的原始碼尚未釋出,有很多實現是猜測的,歡迎提出問題,一起探討!

感覺有用的話麻煩點個贊謝謝,您的點贊是我持續的動力~

相關文章