react hooks 萬字總結

前言

本身在掘金上看了也看了不少關於hooks的文章,感受都講得不是很詳細。並且也有不少的水文。最近本身打算重學react,系統性的再把hooks給學習一遍。html

Hooks is what?

  • react-hooks是react16.8之後,react新增的鉤子API,它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性.
  • Hook是一些可讓你在函數組件裏「鉤入」 React state 及生命週期等特性的函數。

why use Hooks?

類組件的缺點:(來自官網動機react

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

  • 複雜組件變得難以理解編程

  • 難以理解的 classapi

你必須去理解 JavaScript 中 this 的工做方式,這與其餘語言存在巨大差別。還不能忘記綁定事件處理器。沒有穩定的語法提案,代碼很是冗餘。數組

hooks的出現,解決了上面的問題。另外,還有一些其餘的優勢緩存

  • 增長代碼的可複用性,邏輯性,彌補無狀態組件沒有生命週期,沒有數據管理狀態state的缺陷
  • react-hooks思想更趨近於函數式編程。用函數聲明方式代替class聲明方式,雖然說class也是es6構造函數語法糖,可是react-hooks寫起來函數便是組件,無疑也提升代碼的開發效率(無需像class聲明組件那樣寫聲明週期,寫生命週期render函數等)

Hooks沒有破壞性改動

  • 徹底可選的。  你無需重寫任何已有代碼就能夠在一些組件中嘗試 Hook。可是若是你不想,你沒必要如今就去學習或使用 Hook。
  • 100% 向後兼容的。  Hook 不包含任何破壞性改動。
  • 如今可用。  Hook 已發佈於 v16.8.0。

使用Hooks的規則

1. 只在最頂層使用 Hook,不要在循環,條件或嵌套函數中調用 Hook安全

確保老是在你的 React 函數的最頂層調用他們。遵照這條規則,你就能確保 Hook 在每一次渲染中都按照一樣的順序被調用。這讓 React 可以在屢次的 useState 和 useEffect 調用之間保持 hook 狀態的正確。性能優化

2. 只在 React 函數中調用 Hookbabel

不要在普通的 JavaScript 函數中調用 Hook,你能夠:

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

至於爲何會有這些規則,若是你感興趣,請參考Hook 規則

useState

const [state, setState] = useState(initialState)

  • useState 有一個參數(initialState 能夠是一個函數,返回一個值,但通常都不會這麼用),該參數能夠爲任意數據類型,通常用做默認值.
  • useState 返回值爲一個數組,數組的第一個參數爲咱們須要使用的 state,第二個參數爲一個改變state的函數(功能和this.setState同樣)

來看一個計時器的案例

import React,{useState} from "react";
function Example() {
  const [count, setCount] = useState(0);
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
  );
}
export default Example;
複製代碼
  • 第一行:  引入 React 中的 useState Hook。它讓咱們在函數組件中存儲內部 state。
  • 第三行:  在 Example 組件內部,咱們經過調用 useState Hook 聲明瞭一個新的 state 變量。它返回一對值給到咱們命名的變量上。咱們把變量命名爲 count,由於它存儲的是點擊次數。咱們經過傳 0 做爲 useState 惟一的參數來將其初始化爲 0。第二個返回的值自己就是一個函數。它讓咱們能夠更新 count 的值,因此咱們叫它 setCount
  • 第七行:  當用戶點擊按鈕後,咱們傳遞一個新的值給 setCount。React 會從新渲染 Example 組件,並把最新的 count 傳給它。

使用多個state 變量

// 聲明多個 state 變量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '學習 Hook' }]);
複製代碼

沒必要使用多個 state 變量。State 變量能夠很好地存儲對象和數組,所以,你仍然能夠將相關數據分爲一組。

更新State

import React,{useState} from "react";
function Example() {
  const [count, setCount] = useState(0);
  const [person, setPerson] = useState({name:'jimmy',age:22});
  return (
    <div> <p>name {person.name} </p> // 若是新的 state 須要經過使用先前的 state 計算得出,那麼能夠將回調函數當作參數傳遞給 setState。 // 該回調函數將接收先前的 state,並返回一個更新後的值。 <button onClick={() => setCount(count=>count+1)}>Click me</button> <button onClick={() => setPerson({name:'chimmy'})}>Click me</button> </div>
  );
}
export default Example;
複製代碼

setPerson更新person時,不像 class 中的 this.setState,更新 state 變量老是替換它而不是合併它。上例中的person爲{name:'chimmy'} 而不是{name:'chimmy',age:22}

useEffect

Effect Hook 可讓你在函數組件中執行反作用(數據獲取,設置訂閱以及手動更改 React 組件中的 DOM 都屬於反作用)操做

useEffect(fn, array)

useEffect在初次完成渲染以後都會執行一次, 配合第二個參數能夠模擬類的一些生命週期。

若是你熟悉 React class 的生命週期函數,你能夠把 useEffect Hook 看作 componentDidMount``componentDidUpdate 和 componentWillUnmount 這三個函數的組合。

useEffect 實現componentDidMount

若是第二個參數爲空數組,useEffect至關於類組件裏面componentDidMount。

import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("我只會在組件初次掛載完成後執行");
  }, []);
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
  );
}
export default Example;
複製代碼

頁面渲染完成後,會執行一次useEffect。打印「我只會在組件初次掛載完成後執行」,當點擊按鈕改變了state,頁面從新渲染後,useEffect不會執行。

useEffect 實現componentDidUpdate

若是不傳第二個參數,useEffect 會在初次渲染和每次更新時,都會執行。

import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("我會在初次組件掛載完成後以及從新渲染時執行");
  });
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
  );
}
export default Example;
複製代碼

初次渲染時,會執行一次useEffect,打印出「我會在初次組件掛載完成後以及從新渲染時執行」。 當點擊按鈕時,改變了state,頁面從新渲染,useEffect都會執行,打印出「我會在初次組件掛載完成後以及從新渲染時執行」。

useEffect 實現componentWillUnmount

effect 返回一個函數,React 將會在執行清除操做時調用它。

useEffect(() => {
    console.log("訂閱一些事件");
    return () => {
      console.log("執行清除操做")
    }
  },[]);
複製代碼

注意:這裏不僅是組件銷燬時纔會打印「執行清除操做」,每次從新渲染時也都會執行。至於緣由,我以爲官網解釋的很清楚,請參考 解釋: 爲何每次更新的時候都要運行 Effect

控制useEffect的執行

import React, { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(1);
  useEffect(() => {
    console.log("我只會在cout變化時執行");
  }, [count]);
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click cout</button> <button onClick={() => setNumber(number + 1)}>Click number</button> </div>
  );
}
export default Example;
複製代碼

上面的例子,在點擊 click cout按鈕時,纔會打印「我只會在cout變化時執行」。 由於useEffect 的第二個參數的數組裏面的依賴是cout,因此,只有cout發生改變時,useEffect 纔會執行。若是數組中有多個元素,即便只有一個元素髮生變化,React 也會執行 effect。

使用多個 Effect 實現關注點分離

使用 Hook 其中一個目的就是要解決 class 中生命週期函數常常包含不相關的邏輯,但又把相關邏輯分離到了幾個不一樣方法中的問題。

import React, { useState, useEffect } from "react";
function Example() {
  useEffect(() => {
    // 邏輯一
  });
  useEffect(() => {
    // 邏輯二
  });
   useEffect(() => {
    // 邏輯三
  });
  return (
    <div> useEffect的使用 </div>
  );
}
export default Example;
複製代碼

Hook 容許咱們按照代碼的用途分離他們,  而不是像生命週期函數那樣。React 將按照 effect 聲明的順序依次調用組件中的每個 effect。

useEffect中使用異步函數

useEffect是不能直接用 async await 語法糖的

/* 錯誤用法 ,effect不支持直接 async await*/
 useEffect(async ()=>{
        /* 請求數據 */
      const res = await getData()
 },[])
複製代碼

useEffect 的回調參數返回的是一個清除反作用的 clean-up 函數。所以沒法返回 Promise,更沒法使用 async/await

那咱們應該如何讓useEffect支持async/await呢?

方法一(推薦)

const App = () => {
  useEffect(() => {
    (async function getDatas() {
      await getData();
    })();
  }, []);
  return <div></div>;
};
複製代碼

方法二

useEffect(() => {
    const getDatas = async () => {
      const data = await getData();
      setData(data);
    };
    getDatas();
  }, []);
複製代碼

useEffect 作了什麼

經過使用這個 Hook,你能夠告訴 React 組件須要在渲染後執行某些操做。React 會保存你傳遞的函數(咱們將它稱之爲 「effect」),而且在執行 DOM 更新以後調用它。

爲何在組件內部調用 useEffect

將 useEffect 放在組件內部讓咱們能夠在 effect 中直接訪問 count state 變量(或其餘 props)。咱們不須要特殊的 API 來讀取它 —— 它已經保存在函數做用域中。Hook 使用了 JavaScript 的閉包機制,而不用在 JavaScript 已經提供瞭解決方案的狀況下,還引入特定的 React API。

useContext

概念

const value = useContext(MyContext);

接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值。當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值。即便祖先使用 React.memo 或 shouldComponentUpdate,也會在組件自己使用 useContext 時從新渲染。

別忘記 useContext 的參數必須是 context 對象自己

  • 正確:  useContext(MyContext)
  • 錯誤:  useContext(MyContext.Consumer)
  • 錯誤:  useContext(MyContext.Provider)

示例

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// 建立兩個context
export const UserContext = React.createContext();
export const TokenContext = React.createContext();
ReactDOM.render(
  <UserContext.Provider value={{ id: 1, name: "chimmy", age: "20" }}> <TokenContext.Provider value="我是token"> <App /> </TokenContext.Provider> </UserContext.Provider>,
  document.getElementById("root")
);
複製代碼

app.js

import React, { useContext } from "react";
import { UserContext, TokenContext } from "./index";

function Example() {
  let user = useContext(UserContext);
  let token = useContext(TokenContext);
  console.log("UserContext", user);
  console.log("TokenContext", token);
  return (
    <div> name:{user?.name},age:{user?.age} </div>
  );
}
export default Example;
複製代碼

打印的值以下

41PXCT[XET_ZJ7]79K@~6JX.png

提示

若是你在接觸 Hook 前已經對 context API 比較熟悉,那應該能夠理解,useContext(MyContext) 至關於 class 組件中的 static contextType = MyContext 或者 <MyContext.Consumer>

useContext(MyContext) 只是讓你可以讀取 context 的值以及訂閱 context 的變化。你仍然須要在上層組件樹中使用 <MyContext.Provider> 來爲下層組件提供 context。

useReducer

概念

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法。(若是你熟悉 Redux 的話,就已經知道它如何工做了。)

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於以前的 state 等。而且,使用 useReducer 還能給那些會觸發深更新的組件作性能優化,由於你能夠向子組件傳遞 dispatch 而不是回調函數

注意點

React 會確保 dispatch 函數的標識是穩定的,而且不會在組件從新渲染時改變。這就是爲何能夠安全地從 useEffect 或 useCallback 的依賴列表中省略 dispatch

示例

import React, { useReducer } from "react";
export default function Home() {
  function reducer(state, action) {
    switch (action.type) {
      case "increment":
        return { ...state, counter: state.counter + 1 };
      case "decrement":
        return { ...state, counter: state.counter - 1 };
      default:
        return state;
    }
  }
  const [state, dispatch] = useReducer(reducer, { counter: 0 });
  return (
    <div> <h2>Home當前計數: {state.counter}</h2> <button onClick={(e) => dispatch({ type: "increment" })}>+1</button> <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button> </div>
  );
}
複製代碼

useCallback

概念

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
複製代碼

返回一個 [memoized]回調函數。

把內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給通過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將很是有用。

示例

import React, { useState } from "react";
// 子組件
function Childs(props) {
  console.log("子組件渲染了");
  return (
    <> <button onClick={props.onClick}>改標題</button> <h1>{props.name}</h1> </>
  );
}
const Child = React.memo(Childs);
function App() {
  const [title, setTitle] = useState("這是一個 title");
  const [subtitle, setSubtitle] = useState("我是一個副標題");
  const callback = () => {
    setTitle("標題改變了");
  };
  return (
    <div className="App"> <h1>{title}</h1> <h2>{subtitle}</h2> <button onClick={() => setSubtitle("副標題改變了")}>改副標題</button> <Child onClick={callback} name="桃桃" /> </div>
  );
}
複製代碼

執行結果以下圖 image.png

當我點擊改副標題這個 button 以後,副標題會變爲「副標題改變了」,而且控制檯會再次打印出子組件渲染了,這就證實了子組件從新渲染了,可是子組件沒有任何變化,那麼此次 Child 組件的從新渲染就是多餘的,那麼如何避免掉這個多餘的渲染呢?

找緣由

咱們在解決問題的以前,首先要知道這個問題是什麼緣由致使的?

我們來分析,一個組件從新從新渲染,通常三種狀況:

  1. 要麼是組件本身的狀態改變
  2. 要麼是父組件從新渲染,致使子組件從新渲染,可是父組件的 props 沒有改變
  3. 要麼是父組件從新渲染,致使子組件從新渲染,可是父組件傳遞的 props 改變

接下來用排除法查出是什麼緣由致使的:

第一種很明顯就排除了,當點擊改副標題 的時候並無去改變 Child 組件的狀態;

第二種狀況,咱們這個時候用 React.memo 來解決了這個問題,因此這種狀況也排除。

那麼就是第三種狀況了,當父組件從新渲染的時候,傳遞給子組件的 props 發生了改變,再看傳遞給 Child 組件的就兩個屬性,一個是 name,一個是 onClickname 是傳遞的常量,不會變,變的就是 onClick 了,爲何傳遞給 onClick 的 callback 函數會發生改變呢?其實在函數式組件裏每次從新渲染,函數組件都會重頭開始從新執行,那麼這兩次建立的 callback 函數確定發生了改變,因此致使了子組件從新渲染。

用useCallback解決問題

const callback = () => {
  doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])
複製代碼

把函數以及依賴項做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,這個 memoizedCallback 只有在依賴項有變化的時候纔會更新。

那麼只需這樣將傳給Child組件callback函數的改造一下就OK了

const callback = () => { setTitle("標題改變了"); };
// 經過 useCallback 進行記憶 callback,並將記憶的 callback 傳遞給 Child
<Child onClick={useCallback(callback, [])} name="桃桃" />
複製代碼

這樣咱們就能夠看到只會在首次渲染的時候打印出子組件渲染了,當點擊改副標題和改標題的時候是不會打印子組件渲染了的。

useMemo

概念

const cacheSomething = useMemo(create,deps)

  • create:第一個參數爲一個函數,函數的返回值做爲緩存值。
  • deps: 第二個參數爲一個數組,存放當前 useMemo 的依賴項,在函數組件下一次執行的時候,會對比 deps 依賴項裏面的狀態,是否有改變,若是有改變從新執行 create ,獲得新的緩存值。
  • cacheSomething:返回值,執行 create 的返回值。若是 deps 中有依賴項改變,返回的從新執行 create 產生的值,不然取上一次緩存值。

useMemo原理

useMemo 會記錄上一次執行 create 的返回值,並把它綁定在函數組件對應的 fiber 對象上,只要組件不銷燬,緩存值就一直存在,可是 deps 中若是有一項改變,就會從新執行 create ,返回值做爲新的值記錄到 fiber 對象上。

示例

function Child(){
    console.log("子組件渲染了")
    return <div>Child</div> 
}
const Child = memo(Child)
function APP(){
    const [count, setCount] = useState(0);
    const userInfo = {
      age: count,
      name: 'jimmy'
    }
    return <Child userInfo={userInfo}> } 複製代碼

當函數組件從新render時,userInfo每次都將是一個新的對象,不管 count 發生改變沒,都會致使 Child組件的從新渲染。

而下面的則會在 count 改變後纔會返回新的對象。

function Child(){
    console.log("子組件渲染了")
    return <div>Child</div> 
}
function APP(){
    const [count, setCount] = useState(0);
    const userInfo = useMemo(() => {
      return {
        name: "jimmy",
        age: count
      };
    }, [count]);
    return <Child userInfo={userInfo}> } 複製代碼

實際上 useMemo 的做用不止於此,根據官方文檔內介紹:以把一些昂貴的計算邏輯放到 useMemo 中,只有當依賴值發生改變的時候纔去更新。

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

// 計算和的函數,開銷較大
function calcNumber(count) {
  console.log("calcNumber從新計算");
  let total = 0;
  for (let i = 1; i <= count; i++) {
    total += i;
  }
  return total;
}
export default function MemoHookDemo01() {
  const [count, setCount] = useState(100000);
  const [show, setShow] = useState(true);
  const total = useMemo(() => {
    return calcNumber(count);
  }, [count]);
  return (
    <div> <h2>計算數字的和: {total}</h2> <button onClick={e => setCount(count + 1)}>+1</button> <button onClick={e => setShow(!show)}>show切換</button> </div>
  )
}
複製代碼

當咱們去點擊 show切換按鈕時,calcNumber這個計算和的函數並不會出現渲染了.只有count 發生改變時,纔會出現計算.

useCallback 和 useMemo 總結

簡單理解呢 useCallback 與 useMemo 一個緩存的是函數,一個緩存的是函數的返回的結果。useCallback 是來優化子組件的,防止子組件的重複渲染。useMemo 能夠優化當前組件也能夠優化子組件,優化當前組件主要是經過 memoize 來將一些複雜的計算邏輯進行緩存。固然若是隻是進行一些簡單的計算也不必使用 useMemo。

咱們能夠將 useMemo 的返回值定義爲返回一個函數這樣就能夠變通的實現了 useCallback。useCallback(fn, deps) 至關於 useMemo(() => fn, deps)

useRef

const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變

useRef 獲取dom

useRef,它有一個參數能夠做爲緩存數據的初始值,返回值能夠被dom元素ref標記,能夠獲取被標記的元素節點.

import React, { useRef } from "react";
function Example() {
  const divRef = useRef();
  function changeDOM() {
    // 獲取整個div
    console.log("整個div", divRef.current);
    // 獲取div的class
    console.log("div的class", divRef.current.className);
    // 獲取div自定義屬性
    console.log("div自定義屬性", divRef.current.getAttribute("data-clj"));
  }
  return (
    <div> <div className="div-class" data-clj="我是div的自定義屬性" ref={divRef}> 我是div </div> <button onClick={(e) => changeDOM()}>獲取DOM</button> </div>
  );
}
export default Example;
複製代碼

1.png

useRef 緩存數據

useRef還有一個很重要的做用就是緩存數據,咱們知道usestate ,useReducer 是能夠保存當前的數據源的,可是若是它們更新數據源的函數執行一定會帶來整個組件重新執行到渲染,若是在函數組件內部聲明變量,則下一次更新也會重置,若是咱們想要悄悄的保存數據,而又不想觸發函數的更新,那麼useRef是一個很棒的選擇。

下面舉一個,每次換成state 上一次值的例子

import React, { useRef, useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);

  const numRef = useRef(count);

  useEffect(() => {
    numRef.current = count;
  }, [count]);

  return (
    <div> <h2>count上一次的值: {numRef.current}</h2> <h2>count這一次的值: {count}</h2> <button onClick={(e) => setCount(count + 10)}>+10</button> </div>
  );
}
export default Example;
複製代碼

當 ref 對象內容發生變化時,useRef 並不會通知你。變動 .current 屬性不會引起組件從新渲染。因此,上面的例子中雖然numRef.current的值,已經改變了,可是頁面上仍是顯示的上一次的值,從新更新時,纔會顯示上一次更新的值。

寫在最後

若是文章中有什麼錯誤,歡迎指出。

相關文章
相關標籤/搜索