React16:Hooks總覽,擁抱函數式 (這大概是最全的React Hooks吧)

React16.8中加入了Hooks,讓React函數式組件再一次昇華,那麼到底什麼是Hooks?javascript

動機

React官網2018年的React conf上都提到了動機這個東西,那麼出現hooks的動機是什麼?是什麼推進了hooks的出現?先來看一下Hooks的動機。html

1.在組件間複用狀態邏輯很難前端

React沒有提供可複用性行爲「附加」到組件的途徑,在寫類組件的時候,一個類是一個閉包而且state在組件間傳遞並不怎麼友好,雖然可使用props和高階組件來解決,可是這樣會組件的結構更麻煩。若是你在 React DevTools 中觀察過 React 應用,你會發現由 providers,consumers,高階組件,render props 等其餘抽象層組成的組件會造成「嵌套地獄」。java

2. 複雜組件變得難以理解react

React中的類組件是很重的,好比說我就想實現一個很是簡單的功能,必需要帶一堆鉤子函數,讓一個簡單的組件變得很複雜。並且因爲不一樣的生命週期在不一樣的階段調用,致使咱們會在相應的地方做一些處理,有可能把一些徹底不相干的代碼由於執行週期相同必須放在同一個生命週期中,很容易引起bug。es6

3. 難以理解的class算法

文檔上說這點主要是學習class是一個難點。由於我本身寫es6 class有一段時間了,因此class對我本身來講仍是能夠的,而且this理解的還能夠。編程

什麼是Hooks?

那麼什麼是Hook,Hook顧名思義就是鉤子的意思。在函數組件中把React的狀態和生命週期等這些特性鉤入進入,這就是React的Hook。redux

特指代表React的Hook做用是把類組件的一些特性鉤入函數組件中,因在類組件中是不可使Hook的。api

Hooks的使用規則

Hook就是javascript函數,可是使用有兩個規則:

  1. 只能在函數的最外層調用hook。不要在循環、條件判斷或者子函數中調用。(這個關係到了hooks的執行機制,會在下面hook中說到)
  2. 只能在React的函數組件中調用Hook。不要在其餘javascript函數中調用(自定義hooks中也能夠調用)

使用Hooks的好處

  1. 使用hooks,若是業務變動,就不須要把函數組件修改爲類組件。
  2. 告別了繁雜的this和合並了難以記憶的生命週期。
  3. 支持包裝本身的Hooks(自定義Hooks),是基於純命令式的api。
  4. 更好的完成狀態之間的共享,解決原來class組件內部封裝的問題,也解決了高階組件和函數組件的嵌套過深。一個組件一個本身的state,一個組件內能夠公用。

內置的Hook

React一共內置了9種Hook。

  • useState
  • usEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect

useState

之前的函數式組件被成爲純函數組件或者無狀態組件,是隻能接受父組件傳來的props而且只能作展現功能,不能使用state也沒有生命週期。

如今State Hook 可讓函數式組件使用狀態。

useState是React的一個Hook,它是一個方法,能夠傳入值做爲state的默認值,返回一個數組,數組的第一項是對應的狀態(默認值會賦予狀態),數組的第二項是更新狀態的函數。

import React, { useState } from "react";

const Greeting = () => {
    const states = useState(0);
    const count = states[0];
    const setCount = states[1];
    return (
       <> <h1> {count} </h1> <button onClick={() => {setCount(count + 1)}}> + </button> </> ) } export default Greeting; 複製代碼

每次取數組的第幾項太麻煩,因此官方建議使用ES6數組的解構賦值的方式。

const [count, setCount] = useState(1);
複製代碼

看起來是否是簡便多了。更新代碼

import React, { useState } from "react";
const Greeting = () => {
    const [count, setCount] = useState(0);
    return (
       <> <h1> {count} </h1> <button onClick={() => {setCount(count + 1)}}> + </button> </> ) } export default Greeting; 複製代碼

咱們發現,通常函數調用完成以後,其中的變量都會被回收,而上面代碼和圖上能夠看出每次都是在 count的基上相加,並無消失,爲何呢? 先埋下疑問點,在Hook的執行機制會提到。

使用屢次useState

在一個組件中咱們不可能只有一個state,useState容許在一個組件中使屢次,而且每次使用都是一個全新的狀態。

import React, { useState } from "react";
const Greeting = () => {
    const [count, setCount] = useState(0);      //第一次使用
    const [istrue, setIstrue] = useState(true); //第二次使用
    return (
       <> {istrue ? <h1> {count} </h1> : void 0} <button onClick={ () => {setIstrue(!istrue)}}>change</button> <button onClick={() => {setCount(count + 1)}}> + </button> </> ) } export default Greeting; 複製代碼

上面代碼使用兩次useState,完美的完成了功能。

那麼如今又有疑問了,React是怎麼區別屢次調用的hooks的呢?先埋下疑問點,在Hook的執行機制的時候會談到(全部的Hook都是這)。

useEffect

既然React Hooks給了函數式組件(或者說是純函數組件)那麼強大的功能(拋棄類組件),那麼組件中老是要會執行反作用操做,純函數組件保持了函數渲染的純度,那麼要怎麼執行反作用呢?

React Hooks 提供了 Effect Hook,能夠在函數組件中執行反作用操做,而且是在函數渲染DOM完成後執行反作用操做。

import React, {useEffect} from "react";
複製代碼

useEffect這個方法傳入一個函數做爲參數,在函數裏面執行反作用代碼,而且useEffec的第一個參數還支持返回值爲一個函數,這個函數執行至關於組件更新和卸載。

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

const EffectComponent = () => {
    useEffect(() => {
        console.log("useEffect Hook");
    })
    return null;
}
export default EffectComponent
複製代碼

與類組件生命週期的比較

咱們都知道在類組件中能夠在componentDidMountcomponentDidUpdate中執行反作用,那麼在函數組件中useEffect的參數函數就具備類組件的這兩個生命週期的用途,若是useEffec的第一個參數有返回值爲函數的話,函數的返回值至關於componentWillUnmount。能夠說useEffect把這三個API合成了一個。

最多見的作法就是就是在函數參數中寫事件註冊,在函數的返回函數中寫事件銷燬。

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

const EffectComponent = () => {
    const [width, setWidth] = useState(window.innerWidth);
    const resizeHandle = () => {
        setWidth(window.innerWidth);
    }
    useEffect(() => {
        window.addEventListener("resize", resizeHandle);
        return () => {
            window.removeEventListener("resize", resizeHandle)
        }
    })
    return (
        <h1>{width}</h1>
    );
}
export default EffectComponent
複製代碼

useEffect的執行時機

從上面咱們知道了useEffect能夠說是類組件中三種生命週期的結合,可是它的執行時機是什麼樣的呢?從一個小Demo來講

import React, {useState, useEffect} from "react";
const EffectComponent = () => {
    const [count, setCount] = useState(1);
    useEffect(() => {
        console.log("定義事件接口")
        return () => {
            console.log("註銷事件接口")
        }
    })
    return (
        <> {console.log("渲染")} <h1>{count}</h1> <button onClick={() => {setCount(count + 1)}}> + </button> </> ); } export default EffectComponent 複製代碼

在開始的時候有提到,useEffec執行副做時機在渲染後,確實是這樣。細心的你會發現,當我點擊+號的時候,怎麼會出現 註銷事件接口? useEffec函數中的返回函數不是在組件卸載的時候被調用嗎?

我我的的理解是useEffec函數參數中返回函數所表明的銷燬是useEffect本身的銷燬,每次從新執行函數組件都會從新生成新的Effec。假如沒有銷燬,因爲useEffect的函數參數會在首次渲染和更新的時候調用,這就有了一致命的缺點:若是我是定義的事件,每次更新都會執行,那麼豈不是在事件尚未移除掉又定義了一次,因此useEffect加入了這個功能。

咱們來驗證一下上述論述是否正確。

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

const EffectComponent = () => {
    const [width, setWidth] = useState(window.innerWidth);
    const [count, setCount] = useState(1);
    const resizeHandle = () => {
        setWidth(window.innerWidth);
        console.log(window.innerWidth);
    }
    useEffect(() => {
        window.addEventListener("resize", resizeHandle);
        return () => {
            // window.removeEventListener("resize", resizeHandle)
        }
    })
    return (
        <> <h1>{count}</h1> <button onClick={() => {setCount(count + 1)}}>+</button> </> ); } export default EffectComponent 複製代碼

上面代碼我把useEffect 中return的事件移除註釋掉,同時在事件處理函數中打印一下窗口寬度。

能夠看出當我第一次觸發窗口事件的時候,直接打印了三次。

useEffect的第二個參數

當useEffect的第二個參數不寫的話(上面都沒寫),任何更新都會觸發useEffect。那麼下面說一下useEffect的第二個參數。

useEffect的第二個參數是一個數組,表示以來什麼state和props來執行反作用。

數組爲空的時候,useEffect就至關於componentDidMoubtcomponentWillUnmount這兩個生命週期,只在首次渲染和卸載的時候執行。

當數組中值是狀態的時候,就會只監聽這一個狀態的變化。固然數組中能夠多個值,監聽存放state的變化。

const EffectComponent = () => {
    const [count, setCount] = useState(1);
    const [num, setNum] = useState(2);
    useEffect(() => {
        console.log("count狀態更新");
        return () => {
            console.log("useEffect卸載")
        }
    },[count])
    return (
        <> <h1>{count}</h1> <button onClick={() => {setCount(count + 1)}}>+</button> <h1>{num}</h1> <button onClick={() => {setNum(num + 1)}}>+</button> </> ); } 複製代碼

寫多個useEffect

當咱們在寫類組件的時候,一般會把定義事件寫在componentDidMount中,若是隻是一個事件處理,項目不大還好,那若是項目很大,全部的事件處理都定義在一個生命週期中,難道就不亂嗎?亂是確定的,並且還容易出bug。

React Hook 容許函數式組件中定義多個useEffect(和useState相似),多個useEffect互相不受干擾。

const EffectComponent = () => {
    const [count, setCount] = useState(1);
    const [num, setNum] = useState(2);
    useEffect(() => {
        console.log("count狀態更新");
        return () => {
            console.log("count卸載")
        }
    },[count])
    useEffect(() => {
        console.log("num狀態更新");
        return () => {
            console.log("num卸載")
        }
    },[num])
    return (
        <> <h1>{count}</h1> <button onClick={() => {setCount(count + 1)}}>+</button> <h1>{num}</h1> <button onClick={() => {setNum(num + 1)}}>+</button> </> ); } 複製代碼

useEffect在函數組件中的做用很是大,好好利用必成神器。

useContext

React16中更新了Context API,Context主要用於爺孫組件的傳值問題,新的Context API使用訂閱發佈者模式方式實如今爺孫組件中傳值。 在個人博客中我寫了一篇簡單的使用方法Context API,不瞭解的能夠參考一下。

React Hooks出現以後也對Context API出了響應的Hook useContext。一樣也是解傳值的問題。

useContext Hook接受一個context對象(由createContext建立的對象)做爲參數,並返回Context.Consumer。例如:

const stateContext = createContext('default');
複製代碼
  • 正確: useContext(stateContext)
  • 錯誤: useContext(stateContext.Consumer)
  • 錯誤: useContext(stateContext.Provider)

使用方式

好比說有一個簡單的ContextComponent組件

const ContextComponent = () => {
    return (
        <> <h1>{value}</h1> </> ); } 複製代碼

經過Context API給這個組件發信息。

export default () => (
    <stateContext.Provider value={"Hello React"} > <ContextComponent/> </stateContext.Provider> ) 複製代碼

使用useContext()

const value = useContext(stateContext);
複製代碼

使用useContext,必須在函數式組件中,不然會報錯。

能夠看出,使用useContext仍然須要在上層組件中使用<MyContext.Provider>來爲下層組件提供context。

useReducer

看到useReducer,確定會想到Redux,沒錯它和Redux的工做方式是同樣的。useReducer的出現是useState的替代方案,可以讓咱們更好的管理狀態。

useReducer一共能夠接受三個參數並返回當前的state與其配套的dispatch。

第一個參數

useReducer的第一個參數就是形如(state,action) => newState這樣的reducer,沒錯就是reducer,和redux徹底相同。咱們來定義一個簡單的reducer。

const reducer = (state, action) => {
    switch(action.type){
        case "ADD_TODO":
            return [
                ...state,
                action.todo
            ];
        default:
            return state;

    }
}
複製代碼

上面是一個簡單的reducer,細心的你會發現,state參數難道不須要指定一下默認值嗎?不須要,React不須要使用指定state = initialState,有時候初始值須要依賴於props,因此初始值在useReducer上指定,也許已經猜到第二個參數是什麼了?

第二個參數

useReducer的第二個參數和Redux的createStore也相同,指定狀態的默認值。例如:

useReducer(reducer,[{
    id: Date.now(),
    value: "Hello react"
}])
複製代碼

第三個參數

useReducer的第三個參數接受一個函數做爲參數,並把第二個參數看成函數的參數執行。主要做用是初始值的惰性求值,把一些對狀態的邏輯抽離出來,有利於重置state。

定義一個init函數

function init(initialCount) {
    return [
        ...initialCount,
    ];
}
複製代碼

useReducer使用

useReducer(reducer,[
        {
            id: Date.now(),
            value: "Hello react"
        }
    ],init)
複製代碼

useReducer的返回值

useReducer的返回值爲一個數組,數組的第一項爲當前state,第二項爲與當前state對應的dispatch,可使用ES6的解構賦值拿到這兩個

const [state,dispatch] = useReducer(reducer,[
    {
        id: Date.now(),
        value: "Hello react"
    }
],init)
複製代碼

淺比較渲染

若是 Reducer Hook 的返回值與當前 state 相同,React 將跳過子組件的渲染及反作用的執行。

這種方react使用Objec.is比較算法來比較state,所以這是一個淺比較,來測驗一下。

咱們先在reducer中添加一個改變的Todo值的case。

case "CHANGE_TODO":return state[action.id] = 'change' && state;
複製代碼

修改一下return,給下層組件傳一個change屬性

const change = (id) => {
    dispatch({
        type: "CHANGE_TODO",
        id,
    })
}
return (
    <>
        <button onClick={() => {dispatch({type: "ADD_TODO",todo:{id:Date.now(),value:"Hello Hook"}})}}> Add </button>
        {state.map((todo,index) => (
           <Todo key={index} todo={todo.value} change={()=>{change(todo.id)}}/>
        ))}
    </>
)
複製代碼

給Todo組件添加一點擊事件,當點擊觸發上層組件傳來的方法,使組件值修改.

let Todo = ({todo,change}) => {
    return (
        console.log("render"),
        <li onClick={change}>{todo}</li>
    );
}
複製代碼

從圖片上能夠看出,不管我怎麼點擊li都不會發生改變。

那麼咱們來改變一下reducer,讓它返回一個全新的數組。

case "CHANGE_TODO":
    return state.map((todo,index) =>{
        if(todo.id === action.id){
            todo.value="change";
        }
        return todo;
    } )
複製代碼

當返回一個新的數組的時候,點擊li都發生了改變,默認有了 shouldComponentUpdate的功能。

useCallback

useCallback能夠認爲是對依賴項的監聽,把接受一個回調函數和依賴項數組,返回一個該回調函數的memoized(記憶)版本,該回調函數僅在某個依賴項改變時纔會更新。

一個簡單的小例子

const CallbackComponent = () => {
    let [count, setCount] = useState(1);
    let [num, setNum] = useState(1);
    
    const memoized = useCallback( () => {
        return num;
    },[count])
    console.log("記憶:",memoized());
    console.log("原始:",num);
    return (
        <> <button onClick={() => {setCount(count + 1)}}> count+ </button> <button onClick={() => {setNum(num + 1)}}> num+ </button> </> ) } 複製代碼

若是沒有傳入依賴項數組,那麼記憶函數在每次渲染的時候都會更新。

useMemo

useMemo和useCallback很像,惟一不一樣的就是

useCallback(fn, deps) 至關於 useMemo(() => fn, deps

這裏就不過多介紹了。

useRef

React16出現了可用Object.createRef建立ref的方法,所以也出了這樣一個Hook。

使用語法:

const refContainer = useRef(initialValue);

useRef返回一個可變的ref對象,useRef接受一個參數綁定在返回的ref對象的current屬性上,返回的ref對象在整個生命週期中保持不變。

栗子:

const RefComponent = () => {
    let inputRef = useRef(null);
    useEffect(() => {
        inputRef.current.focus();
    })
    return (
        <input type="text" ref={inputRef}/>
    ) 
}
複製代碼

上面例子在input上綁定一個ref,使得input在渲染後自動焦點聚焦。

useImperativeHandle

useImperativeHandle 可讓你在使用 ref 時自定義暴露給父組件的實例值。

就是說:當咱們使用父組件把ref傳遞給子組件的時候,這個Hook容許在子組件中把自定義實例附加到父組件傳過來的ref上,有利於父組件控制子組件。

使用方式

useImperativeHandle(ref, createHandle, [deps])

一個栗子:

function FancyInput(props, ref) {
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current.value="Hello";
        }
    }));
    return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);

export default () => {
    let ref = useRef(null);
    useEffect(() => {
        console.log(ref);
        ref.current.focus();
    })
    return (
        <>
            <FancyInput ref={ref}/>
        </>
    )
}
複製代碼

上面是一個父子組件中ref傳遞的例子,使用到了forwardRef(這是一個高階函數,主要用於ref在父子組件中的傳遞),使用useImperativeHandle把第二個參數的返回值綁定到父組件傳來的ref上。

useLayoutEffect

這個鉤子函數和useEffect相同,都是用來執行反作用。可是它會在全部的DOM變動以後同步調用effect。useLayoutEffect和useEffect最大的區別就是一個是同步一個是異步。

從這個Hook的名字上也能夠看出,它主要用來讀取DOM佈局並觸發同步渲染,在瀏覽器執行繪製以前,useLayoutEffect 內部的更新計劃將被同步刷新。

官網建議仍是儘量的是使用標準的useEffec以免阻塞視覺更新。

Hook的執行機制

上面一共埋了2個疑問點。

第一個:函數調用完以後會把函數中的變量清除,但ReactHook是怎麼複用狀態呢?

React 保持對當先渲染中的組件的追蹤,每一個組件內部都有一個「記憶單元格」列表。它們只不過是咱們用來存儲一些數據的 JavaScript 對象。當你用 useState() 調用一個Hook的時候,它會讀取當前的單元格(或在首次渲染時將其初始化),而後把指針移動到下一個。這就是多個 useState() 調用會獲得各自獨立的本地 state 的緣由。

之因此不叫createState,而是叫useState,由於 state 只在組件首次渲染的時候被建立。在下一次從新渲染時,useState 返回給咱們當前的 state。

const [count, setCount] = useState(1);
    setCount(2);
    //第一次渲染
        //建立state,
        //設置count的值爲2
    //第二次渲染
        //useState(1)中的參數忽略,並把count賦予2
複製代碼

React是怎麼區分屢次調用的hooks的呢,怎麼知道這個hook就是這個做用呢?

React 靠的是 Hook 調用的順序。在一個函數組件中每次調用Hooks的順序是相同。藉助官網的一個例子:

// ------------
// 首次渲染
// ------------
useState('Mary')           // 1. 使用 'Mary' 初始化變量名爲 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操做
useState('Poppins')        // 3. 使用 'Poppins' 初始化變量名爲 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新標題

// -------------
// 二次渲染
// -------------
useState('Mary')           // 1. 讀取變量名爲 name 的 state(參數被忽略)
useEffect(persistForm)     // 2. 替換保存 form 的 effect
useState('Poppins')        // 3. 讀取變量名爲 surname 的 state(參數被忽略)
useEffect(updateTitle)     // 4. 替換更新標題的 effect

// ...
複製代碼

在上面hook規則的時候提到Hook必定要寫在函數組件的對外層,不要寫在判斷、循環中,正是由於要保證Hook的調用順序相同。

若是有一個Hook寫在了判斷語句中

if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
}
複製代碼

藉助上面例子,若是說name是一個表單須要提交的值,在第一次渲染中,name不存在爲true,因此第一次Hook的執行順序爲

useState('Mary')           // 1. 使用 'Mary' 初始化變量名爲 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操做
useState('Poppins')        // 3. 使用 'Poppins' 初始化變量名爲 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新標題
複製代碼

在第二次渲染中,若是有表單中有信息填入,那麼name就不等於空,Hook的渲染順序以下:

useState('Mary')           // 1. 讀取變量名爲 name 的 state(參數被忽略)
// useEffect(persistForm) // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (以前爲 3)。讀取變量名爲 surname 的 state 失敗
useEffect(updateTitle)     // 🔴 3 (以前爲 4)。替換更新標題的 effect 失敗
複製代碼

這樣就會引起Bug的出現。所以在寫Hook的時候必定要在函數組件的最外層寫,不要寫在判斷,循環中。

自定義Hook

自定義hooks能夠說成是一種約定而不是功能。當一個函數以use開頭而且在函數內部調用其餘hooks,那麼這個函數就能夠成爲自定義hooks,好比說useSomething

自定義Hooks能夠封裝狀態,可以更好的實現狀態共享。

咱們來封裝一個數字加減的Hook

const useCount = (num) => {
    let [count, setCount] = useState(num);
    return [count,()=>setCount(count + 1), () => setCount(count - 1)]
};
複製代碼

這個自定義Hook內部使用useState定義一個狀態,返回一個數組,數組中有狀態的值、狀態++的函數,狀態--的函數。

const CustomComp = () => {
    let [count, addCount, redCount] = useCount(1);

    return (
        <> <h1>{count}</h1> <button onClick={addCount}> + </button> <button onClick={redCount}> - </button> </> ) } 複製代碼

主函數中使用解構賦值的方式接受這三個值使用,這是一種很是簡單的自定義Hook。若是項目大的話使用自定義Hook會抽離能夠抽離公共代碼,極大的減小咱們的代碼量,提升開發效率。

總結

Hooks的學習就總結到這裏。在學習的過程當中總結知識,並推廣給志同道合的同伴,這無疑是我努力學好它的動力。學習React不算太長,但在學習過程當中到處都是對React中運用函數式編程和軟件工程的驚歎,前端的路還有很長,我只不過才半腳踏入門,努力向本身的目標前進。

相關文章
相關標籤/搜索