React Hook 不徹底指南

前言

React HookReact16.8.0版本以後提出的新增特性,因爲以前的項目都不怎麼用到React,所以也就匆匆瞭解一下,最近由於換工做,主要技術棧變爲React了,因此須要着重研究一下React的一些特性以更好地應用到項目開發中和更好地進行知識沉澱。javascript

Hook是什麼

在解釋這個問題以前,能夠先看一段代碼:css

import React, { useState } from 'react'
function Example() {
    // 聲明一個叫 "count" 的 state 變量
    const [count, setCount] = useState(0)
    // 與 componentDidMount and componentDidUpdate效果相似
    useEffect(() => {
        // Update the document title using the browser API
        document.title = `You clicked ${count} times`
    })

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

Hook是一個特殊的函數,它可讓你「鉤入」React的特性。例如,useState是容許你在React函數組件中添加stateHook。若是你在編寫函數組件並意識到須要向其添加一些state,之前的作法是必須將其它轉化爲class。如今你能夠在現有的函數組件中使用Hook;又例如useEffect Hook能夠告訴React組件須要在渲染後執行某些操做,React會保存你傳遞的函數(咱們將它稱之爲 「effect」),而且在執行 DOM 更新以後調用它,能夠把它看作componentDidMountcomponentDidUpdatecomponentWillUnmount這三個函數的組合。html

官方解釋:Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。

爲何要提出React Hook

在組件之間複用狀態邏輯很難

React沒有提供將可複用性行爲「附加」到組件的途徑(例如,把組件鏈接到 store)。若是你使用過 React一段時間,你也許會熟悉一些解決此類問題的方案,好比 render props高階組件。可是這類方案須要從新組織你的組件結構,這可能會很麻煩,使你的代碼難以理解。若是你在 React DevTools中觀察過 React應用,你會發現由 providers,consumers,高階組件,render props等其餘抽象層組成的組件會造成「嵌套地獄」。儘管咱們能夠在 DevTools過濾掉它們,但這說明了一個更深層次的問題: React須要爲共享狀態邏輯提供更好的原生途徑。

Hook能夠在無需修改組件結構的狀況下複用狀態邏輯,這使得在組件間或社區內共享Hook變得更便捷java

複雜組件變得難以理解

咱們常常維護一些組件,組件起初很簡單,但隨着業務複雜度的提高,組件逐漸會變得比較複雜,使得每一個生命週期經常包含一些不相關的邏輯。例如,組件經常在componentDidMountcomponentDidUpdate中獲取數據。可是,同一個componentDidMount中可能也包含不少其它的邏輯,如設置事件監聽,而以後需在componentWillUnmount中清除。相互關聯且須要對照修改的代碼被進行了拆分,而徹底不相關的代碼卻在同一個方法中組合在一塊兒,如此很容易產生bug,而且致使邏輯不一致,維護起來也會顯得比較吃力。react

爲了解決這個問題,Hook將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據),而並不是強制按照生命週期劃分。你還可使用reducer來管理組件的內部狀態,使其更加可預測。npm

難以理解的 class

引用官方的話:編程

除了代碼複用和代碼管理會遇到困難外,咱們還發現 class是學習 React的一大屏障。你必須去理解 JavaScriptthis的工做方式,這與其餘語言存在巨大差別。還不能忘記綁定事件處理器。沒有穩定的語法提案,這些代碼很是冗餘。你們能夠很好地理解 propsstate和自頂向下的數據流,但對 class卻束手無策。即使在有經驗的 React開發者之間,對於函數組件與 class組件的差別也存在分歧,甚至還要區分兩種組件的使用場景。

另外,React已經發布五年了,咱們但願它能在下一個五年也與時俱進。就像SvelteAngularGlimmer等其它的庫展現的那樣,組件預編譯會帶來巨大的潛力。尤爲是在它不侷限於模板的時候。最近,咱們一直在使用Prepack來試驗component folding,也取得了初步成效。可是咱們發現使用class組件會無心中鼓勵開發者使用一些讓優化措施無效的方案。class也給目前的工具帶來了一些問題。例如,class不能很好的壓縮,而且會使熱重載出現不穩定的狀況。所以,咱們想提供一個使代碼更易於優化的APIsegmentfault

Hook可以在非class的狀況下使用更多的React特性。 其實, React組件一直更像是函數。而Hook則擁抱了函數,同時也沒有犧牲React的精神原則。Hook提供了問題的解決方案,無需學習複雜的函數式或響應式編程技術。數組

最重要的是,Hook是向下兼容的,它和現有代碼能夠同時工做,你能夠漸進式地使用他們,不用急着遷移到Hook瀏覽器

內置經常使用HOOK概覽

React中內置的Hook API

  • 基礎Hook

    • useState
    • useEffect
    • useContext
  • 額外的Hook

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

State Hook

能夠看下面的代碼:

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <h1>這是一個示例</h1>
      <div>點擊了{count}次</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        點擊
      </button>
      <button
        onClick={() => {
          setCount(0);
        }}
      >
        清除
      </button>
    </div>
  );
}

上述代碼中,useState就是一個Hook。經過在函數組件裏調用它來給組件添加一些內部stateReact會在重複渲染時保留這個stateuseState會返回一對值:當前狀態和一個讓你更新它的函數,你能夠在事件處理函數中或其餘一些地方調用這個函數。它相似class組件的this.setState,可是它不會把新的state和舊的state進行合併。

useState惟一的參數就是初始state。在上面的例子中,咱們的計數器是從零開始的,因此初始state就是0。值得注意的是,不一樣於this.state,這裏的state不必定要是一個對象,但若是你有須要,它也能夠是。這個初始state參數只有在第一次渲染時會被用到。

你也能夠在函數組件中屢次使用state Hook

調用 useState 方法的時候作了什麼?

它定義一個 「state 變量」。在上面的示例中該變量叫count, 但它能夠是任意的變量名,好比banana。這是一種在函數調用時保存變量的方式,useState是一種新方法,它與class裏面的this.state提供的功能徹底相同。通常來講,在函數退出後變量就會」消失」,而state中的變量會被React保留。

useState 須要哪些參數?

useState()方法裏面惟一的參數就是初始state。不一樣於class的是,咱們能夠按照須要使用數字或字符串對其進行賦值,而不必定是對象。在示例中,只需使用數字來記錄用戶點擊次數,因此咱們傳了0 做爲變量的初始state。(若是咱們想要在state中存儲兩個不一樣的變量,只需調用useState()兩次便可。)

useState 方法的返回值是什麼?

返回值爲:當前state以及更新state的函數。這就是咱們寫 const [count, setCount] = useState() 的緣由。這與class裏面this.state.countthis.setState相似,惟一區別就是你須要成對的獲取它們。

繼續深刻

每一次渲染都有它本身的props和state

咱們直接看代碼來方便理解:

function Counter() {
  const [count, setCount] = useState(0);

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

<p>You clicked {count} times</p>看到這段代碼,有必定經驗的人可能會想,這其中的原理是否是經過watcher,或者是data binding或者是proxy來實現的呢?都不是,count僅僅只是一個數字類型的變量而已,不是上述中的任何一個,就像下面的普通的變量賦值同樣:

const count = 42;
// ...
<p>You clicked {count} times</p>

組件在第一次渲染的時候,從useState()拿到count的初始值0。當咱們調用setCount(1)React會再次渲染組件,這一次count1。就如同下面示例的同樣:

// During first render
function Counter() {
  const count = 0; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// After a click, our function is called again
function Counter() {
  const count = 1; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// After another click, our function is called again
function Counter() {
  const count = 2; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

當咱們更新狀態的時候,React會從新渲染組件。每一次渲染都能拿到獨立的count狀態,這個狀態值是函數中的一個常量

因此下面的這行代碼沒有作任何特殊的數據綁定:

<p>You clicked {count} times</p>

它僅僅只是在渲染輸出中插入了count這個數字。這個數字由React提供。當setCount的時候,React會帶着一個不一樣的count值再次調用組件。而後,React會更新DOM以保持和渲染輸出一致。

這裏關鍵的點在於任意一次渲染中的count常量都不會隨着時間改變。渲染輸出會變是由於咱們的組件被一次次調用,而每一次調用引發的渲染中,它包含的count值獨立於其餘渲染。

Effect Hook

什麼是反作用?React官網是這麼定義的:

你以前可能已經在 React組件中執行過數據獲取、訂閱或者手動修改過 DOM。咱們統一把這些操做稱爲「反作用」,或者簡稱爲「做用」。

useEffect就是一個Effect Hook,給函數組件增長了操做反作用的能力。它跟class組件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount具備相同的用途,只不過被合併成了一個API

例如,下面這個組件在React更新DOM後會設置一個頁面標題:

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

function Example() {
  const [count, setCount] = useState(0);

  // 至關於 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用瀏覽器的 API 更新頁面標題
    document.title = `You clicked ${count} times`;
  });

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

當你調用useEffect時,就是在告訴React在完成對DOM的更改後運行你的「反作用」函數。因爲反作用函數是在組件內聲明的,因此它們能夠訪問到組件的propsstate。默認狀況下,React會在每次渲染後調用反作用函數,包括第一次渲染的時候。

反作用函數還能夠經過返回一個函數來指定如何「清除」反作用。例如,在下面的組件中使用反作用函數來訂閱好友的在線狀態,並經過取消訂閱來進行清除操做:

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

在這個示例中,React會在組件銷燬時取消對ChatAPI的訂閱,而後在後續渲染時從新執行反作用函數。

useState同樣,你能夠在組件中屢次使用useEffect。經過使用Hook,你能夠把組件內相關的反作用組織在一塊兒(例如建立訂閱及取消訂閱),而不要把它們拆分到不一樣的生命週期函數裏。這樣就有利於你對代碼的維護。也再一次說明了React官方爲何會使用Hook

深刻

每次渲染都有它本身的Effects

再次看到官網文檔中的例子:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

那麼:effect是如何讀取到最新的count狀態值的呢?

也許,是某種data bindingwatching機制使得count可以在effect函數內更新?也或許count是一個可變的值,React會在咱們組件內部修改它以使咱們的effect函數總能拿到最新的值?

都不是。

咱們已經知道count是某個特定渲染中的常量。事件處理函數「看到」的是屬於它那次特定渲染中的count狀態值。對於effects也一樣如此:

並非count的值在「不變」的effect中發生了改變,而是effect函數自己在每一次渲染中都不相同。

每個effect版本「看到」的count值都來自於它屬於的那次渲染:

// During first render
function Counter() {
  // ...
  useEffect(
    // Effect function from first render
    () => {
      document.title = `You clicked ${0} times`;
    }
  );
  // ...
}

// After a click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from second render
    () => {
      document.title = `You clicked ${1} times`;
    }
  );
  // ...
}

// After another click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from third render
    () => {
      document.title = `You clicked ${2} times`;
    }
  );
  // ..
}

React會記住你提供的effect函數,而且會在每次更改做用於DOM並讓瀏覽器繪製屏幕後去調用它。

因此雖然咱們說的是一個effect(這裏指更新documenttitle),但其實每次渲染都是一個不一樣的函數 — 而且每一個effect函數「看到」的propsstate都來自於它屬於的那次特定渲染。

Hook使用規則

Hook 就是 JavaScript 函數,可是使用它們會有兩個額外的規則:

  • 只能在函數最外層調用 Hook不要在循環、條件判斷或者子函數中調用。
  • 只能在React的函數中調用調用Hook。不要在普通的JavaScript函數中調用Hook,你能夠:

    • React的函數組件中調用Hook
    • 在自定義Hook中調用其餘Hook

爲了更好地執行這個規則,react提供了eslint插件幫助你去檢測和強制執行上述規則:eslint-plugin-react-hooks

爲何是這樣的規則呢?

這要從React內部執行Hook的機制提及:

React函數組件中,可使用多個useState或者useEffect,那麼React怎麼知道哪一個state對應哪一個useState?答案是React靠的是Hook調用的順序。只要Hook的調用順序在屢次渲染之間保持一致,React就能正確地將內部state和對應的Hook進行關聯。若是咱們將一個Hook調用放在了條件語句中,就有可能會擾亂Hook的調用的順序,致使內部錯誤的對應state和useState,進而致使bug的產生。

自定義Hook

自定義Hook是一個函數,其名稱以 「use」 開頭,函數內部能夠調用其餘的Hook
當咱們想在兩個函數之間共享邏輯時,咱們會把它提取到第三個函數中。而組件和Hook都是函數,因此也一樣適用這種方式。

能夠直接看下面的例子:

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

React組件不一樣的是,自定義Hook不須要具備特殊的標識。咱們能夠自由的決定它的參數是什麼,以及它應該返回什麼。換句話說,它就像一個正常的函數,可是它的名字應該始終以use開頭,這樣能夠一眼看出其符合Hook的規則。

此處useFriendStatusHook目的是訂閱某個好友的在線狀態。這就是咱們須要將friendID做爲參數,並返回這位好友的在線狀態的緣由。

使用自定義Hook

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

自定義Hook是一種天然遵循Hook設計的約定,而並非React的特性。

自定義Hook必須以 「use」 開頭嗎?必須如此。這個約定很是重要。不遵循的話,因爲沒法判斷某個函數是否包含對其內部Hook的調用,React將沒法自動檢查你的Hook是否違反了Hook的規則。

在兩個組件中使用相同的Hook會共享state?不會。自定義Hook是一種重用狀態邏輯的機制(例如設置爲訂閱並存儲當前值),因此每次使用自定義Hook時,其中的全部state和反作用都是徹底隔離的。

自定義Hook如何獲取獨立的state?每次調用Hook,它都會獲取獨立的state。因爲咱們直接調用了useFriendStatus,從React的角度來看,咱們的組件只是調用了useStateuseEffect。正如咱們在以前章節中瞭解到的同樣,咱們能夠在一個組件中屢次調用useStateuseEffect,它們是徹底獨立的。

總結

零零碎碎寫了這麼多,做爲一個入門參考,看了這篇文章,應該會對React Hook有了大體的瞭解,文章中也有深刻其內部機制剖析的地方,可是僅僅對state和effect部分作了簡要的深刻,而實際上React Hook中間還有不少的點值得去深刻推敲,因爲實際項目工做中用到的很少,所以也無法抓住某個坑作深刻的研究,準備後續認真研讀一下react的源碼,對其內部機制作深刻的研究。好好靜下心來沉澱。

參考文檔

相關文章
相關標籤/搜索