一. Hook高級使用

1.1. useReducer

很多人看到useReducer的第一反應應該是redux的某個替代品,其實並不是。

useReducer僅僅是useState的一種替代方案:

  • 在某些場景下,如果state的處理邏輯比較複雜,我們可以通過useReducer來對其進行拆分;

  • 或者這次修改的state需要依賴之前的state時,也可以使用;

單獨創建一個reducer/counter.js文件:

export function counterReducer(state, action) {
  switch(action.type) {
    case "increment":
      return {...state, counter: state.counter + 1}
    case "decrement":
      return {...state, counter: state.counter - 1}
    default:
      return state;
  }
}

home.js

import React, { useReducer } from 'react'
import { counterReducer } from '../reducer/counter'

export default function Home() {
  const [state, dispatch] = useReducer(counterReducer, {counter: 100});

  return (
    <div>
      <h2>當前計數: {state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
    </div>
  )
}

我們來看一下,如果我們創建另外一個profile.js也使用這個reducer函數,是否會進行數據的共享:

import React, { useReducer } from 'react'
import { counterReducer } from '../reducer/counter'

export default function Profile() {
  const [state, dispatch] = useReducer(counterReducer, {counter: 0});

  return (
    <div>
      <h2>當前計數: {state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
    </div>
  )
}

數據是不會共享的,它們只是使用了相同的counterReducer的函數而已。

所以,useReducer只是useState的一種替代品,並不能替代Redux。

1.2. useCallback

useCallback實際的目的是爲了進行性能的優化。

如何進行性能的優化呢?

  • useCallback會返回一個函數的 memoized(記憶的) 值;

  • 在依賴不變的情況下,多次定義的時候,返回的值是相同的;

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

我們來看下面一段很有趣的代碼:

  • increment1在每次函數組件重新渲染時,會返回相同的值;

  • increment2每次定義的都是不同的值;

  • 問題:是否increment1會比increment2更加節省性能呢?

    • 事實上,經過一些測試,並沒有更加節省內存,因爲useCallback中還是會傳入一個函數作爲參數;

    • 所以並不存在increment2每次創建新的函數,而increment1不需要創建新的函數這種性能優化;

  • 那麼,爲什麼說useCallback是爲了進行性能優化呢?

import React, { memo, useState, useCallback } from 'react'

export default function CallbackHookDemo() {
  const [count, setCount] = useState(0);

  const increment1 = useCallback(function increment() {
    setCount(count + 1);
  }, []);

  const increment2 = function() {
    setCount(count + 1);
  }

  return (
    <div>
      <h2>當前計數: {count}</h2>
      <button onClick={increment1}>+1</button>
      <button onClick={increment2}>+1</button>
    </div>
  )
}

我們來對上面的代碼進行改進:

  • 在下面的代碼中,我們將回調函數傳遞給了子組件,在子組件中會進行調用;

  • 在發生點擊時,我們會發現接受increment1的子組件不會重新渲染,但是接受increment2的子組件會重新渲染;

  • 所以useCallback最主要用於性能渲染的地方應該是和memo結合起來,決定子組件是否需要重新渲染;

import React, { memo, useState, useCallback } from 'react';

const CounterIncrement = memo((props) => {
  console.log("CounterIncrment被渲染:", props.name);
  return <button onClick={props.increment}>+1</button>
})

export default function CallbackHookDemo() {
  const [count, setCount] = useState(0);

  const increment1 = useCallback(function increment() {
    setCount(count + 1);
  }, []);

  const increment2 = function() {
    setCount(count + 1);
  }

  return (
    <div>
      <h2>當前計數: {count}</h2>
      {/* <button onClick={increment1}>+1</button>
      <button onClick={increment2}>+1</button> */}
      <CounterIncrement increment={increment1} name="increment1"/>
      <CounterIncrement increment={increment2} name="increment2"/>
    </div>
  )
}

1.3. useMemo

useMemo實際的目的也是爲了進行性能的優化。

如何進行性能的優化呢?

  • useMemo返回的也是一個 memoized(記憶的) 值;

  • 在依賴不變的情況下,多次定義的時候,返回的值是相同的;

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

我們來看一個案例:

  • 無論我們點擊了是 +1 還是 切換 案例都會重新計算一次;
  • 事實上,我們只是希望在count發生變化時重新計算;

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

function calcNum(count) {
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += i;
  }
  console.log("計算一遍");
  return total
}

export default function MemoHookDemo() {
  const [count, setCount] = useState(10);
  const [isLogin, setIsLogin] = useState(true);

  const total = calcNum(count);

  return (
    <div>
      <h2>數字和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      {isLogin && <h2>Coderwhy</h2>}
      <button onClick={e => setIsLogin(!isLogin)}>切換</button>
    </div>
  )
}

這個時候,我們可以使用useMemo來進行性能的優化:

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

function calcNum(count) {
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += i;
  }
  console.log("計算一遍");
  return total
}

export default function MemoHookDemo() {
  const [count, setCount] = useState(10);
  const [isLogin, setIsLogin] = useState(true);

  const total = useMemo(() => {
    return calcNum(count);
  }, [count]);

  return (
    <div>
      <h2>數字和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      {isLogin && <h2>Coderwhy</h2>}
      <button onClick={e => setIsLogin(!isLogin)}>切換</button>
    </div>
  )
}

當然,useMemo也可以用於子組件的性能優化:

  • ShowCounter子組件依賴的是一個基本數據類型,所以在比較的時候只要值不變,那麼就不會重新渲染;

  • ShowInfo接收的是一個對象,每次都會定義一個新的對象,所以我們需要通過useMemo來對其進行優化;

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

function calcNum(count) {
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += i;
  }
  console.log("計算一遍");
  return total
}

const ShowCounter = memo((props) => {
  console.log("重新渲染");
  return <h1>Counter: {props.total}</h1>
})

const ShowInfo = memo((props) => {
  console.log("ShowInfo重新渲染");
  return <h1>信息: {props.info.name}</h1>
})

export default function MemoHookDemo() {
  const [count, setCount] = useState(10);
  const [isLogin, setIsLogin] = useState(true);

  const total = useMemo(() => {
    return calcNum(count);
  }, [count]);

  const info = useMemo(() => {
    return {name: "why"}
  }, [])

  return (
    <div>
      <h2>數字和: {total}</h2>
      <ShowCounter total={total} />
      <ShowInfo info={info}/>
      <button onClick={e => setCount(count + 1)}>+1</button>
      {isLogin && <h2>Coderwhy</h2>}
      <button onClick={e => setIsLogin(!isLogin)}>切換</button>
    </div>
  )
}

1.4. useRef

useRef返回一個ref對象,返回的ref對象在組件的整個生命週期保持不變。

最常用的ref是兩種用法:

  • 用法一:引入DOM(或者組件,但是需要是class組件)元素;

  • 用法二:保存一個數據,這個對象在整個生命週期中可以保存不變;

用法一:引用DOM

import React, { useRef } from 'react';

export default function RefHookDemo() {
  const inputRef = useRef();
  const titleRef = useRef();

  const handleOperating = () => {
    titleRef.current.innerHTML = "我是coderwhy";
    inputRef.current.focus();
  }

  return (
    <div>
      <input type="text" ref={inputRef}/>
      <h2 ref={titleRef}>默認內容</h2>

      <button onClick={e => handleOperating()}>操作</button>
    </div>
  )
}

用法二:使用ref保存上一次的某一個值

  • useRef可以想象成在ref對象中保存了一個.current的可變盒子;

  • useRef在組件重新渲染時,返回的依然是之前的ref對象,但是current是可以修改的;

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

let preValue = 0;

export default function RefHookDemo02() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

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

  return (
    <div>
      <h2>前一次的值: {countRef.current}</h2>
      <h2>這一次的值: {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
}

1.5. useImperativeHandle

useImperativeHandle並不是特別好理解,我們一點點來學習。

我們先來回顧一下ref和forwardRef結合使用:

  • 通過forwardRef可以將ref轉發到子組件;

  • 子組件拿到父組件中創建的ref,綁定到自己的某一個元素中;

import React, { useRef, forwardRef } from 'react';

const HYInput = forwardRef(function (props, ref) {
  return <input type="text" ref={ref}/>
})

export default function ForwardDemo() {
  const inputRef = useRef();

  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e => inputRef.current.focus()}>聚焦</button>
    </div>
  )
}

上面的做法本身沒有什麼問題,但是我們是將子組件的DOM直接暴露給了父組件:

  • 直接暴露給父組件帶來的問題是某些情況的不可控;

  • 父組件可以拿到DOM後進行任意的操作;

  • 但是,事實上在上面的案例中,我們只是希望父組件可以操作的focus,其他並不希望它隨意操作;

通過useImperativeHandle可以只暴露固定的操作:

  • 通過useImperativeHandle的Hook,將 傳入的refuseImperativeHandle第二個參數返回的對象 綁定到了一起;
  • 所以在父組件中,使用 inputRef.current 時,實際上使用的是 返回的對象
  • 比如我調用了 focus函數 ,甚至可以調用 printHello函數
import React, { useRef, forwardRef, useImperativeHandle } from 'react';

const HYInput = forwardRef(function (props, ref) {
  // 創建組件內部的ref
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    printHello: () => {
      console.log("Hello World")
    }
  }))

  // 這裏綁定的是組件內部的inputRef
  return <input type="text" ref={inputRef}/>
})

export default function ImperativeHandleHookForwardDemo() {
  const inputRef = useRef();

  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e => inputRef.current.focus()}>聚焦</button>
      <button onClick={e => inputRef.current.printHello()}>Hello World</button>
    </div>
  )
}

1.6. useLayoutEffect

useLayoutEffect看起來和useEffect非常的相似,事實上他們也只有一點區別而已:

  • useEffect會在渲染的內容更新到DOM上後執行,不會阻塞DOM的更新;

  • useLayoutEffect會在渲染的內容更新到DOM上之前執行,會阻塞DOM的更新;

如果我們希望在某些操作發生之後再更新DOM,那麼應該將這個操作放到useLayoutEffect。

我們來看下面的一段代碼:

  • 這段代碼在開發中會發生閃爍的現象;

  • 因爲我們先將count設置爲了0,那麼DOM會被更新,並且會執行一次useEffect中的回調函數;

  • 在useEffect中我們發現count爲0,又執行一次setCount操作,那麼DOM會再次被更新,並且useEffect又會被執行一次;

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

export default function EffectHookDemo() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count === 0) {
      setCount(Math.random()*200)
    }
  }, [count]);

  return (
    <div>
      <h2>當前數字: {count}</h2>
      <button onClick={e => setCount(0)}>隨機數</button>
    </div>
  )
}

事實上,我們上面的操作的目的是在count被設置爲0時,隨機另外一個數字:

  • 如果我們使用useLayoutEffect,那麼會等到useLayoutEffect代碼執行完畢後,再進行DOM的更新;

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

export default function EffectHookDemo() {
  const [count, setCount] = useState(0);

  useLayoutEffect(() => {
    if (count === 0) {
      setCount(Math.random()*200)
    }
  }, [count]);

  return (
    <div>
      <h2>當前數字: {count}</h2>
      <button onClick={e => setCount(0)}>隨機數</button>
    </div>
  )
}
useEffect和useLayoutEffect對比

二. 自定義Hook

2.1. 認識自定義hook

自定義Hook本質上只是一種函數代碼邏輯的抽取,嚴格意義上來說,它本身並不算React的特性。

需求:所有的組件在創建和銷燬時都進行打印

  • 組件被創建:打印 組件被創建了
  • 組件被銷燬:打印 組件被銷燬了
export default function CustomHookDemo() {
  useEffect(() => {
    console.log("組件被創建了");
    return () => {
      console.log("組件被銷燬了");
    }
  }, [])

  return (
    <div>
      <h2>CustomHookDemo</h2>
    </div>
  )
}

但是這樣來做意味着所有的組件都需要有對應的邏輯:

function Home(props) {
  useEffect(() => {
    console.log("組件被創建了");
    return () => {
      console.log("組件被銷燬了");
    }
  }, [])
  return <h2>Home</h2>
}

function Profile(props) {
  useEffect(() => {
    console.log("組件被創建了");
    return () => {
      console.log("組件被銷燬了");
    }
  }, [])
  return <h2>Profile</h2>
}

如何可以對它們的邏輯進行抽取呢?

  • 我們可能希望抽取到一個函數中;

function loggingLife() {
  useEffect(() => {
    console.log("組件被創建了");
    return () => {
      console.log("組件被銷燬了");
    }
  }, [])
}

但是,抽取到這裏調用之後,代碼是報錯的:

  • 原因是普通的函數中不能使用hook

image-20200729101732525

那麼,我們應該如何操作呢?

  • 非常簡單,函數以特殊的方式命名,以 use 開頭即可;
function useLoggingLife() {
  useEffect(() => {
    console.log("組件被創建了");
    return () => {
      console.log("組件被銷燬了");
    }
  }, [])
}

當然,自定義Hook可以有參數,也可以有返回值:

function useLoggingLife(name) {
  useEffect(() => {
    console.log(`${name}組件被創建了`);
    return () => {
      console.log(`${name}組件被銷燬了`);
    }
  }, [])
}

2.2. 自定義Hook練習

我們通過一些案例來練習一下自定義Hook。

使用User、Token的Context

比如多個組件都需要使用User和Token的Context:

  • 這段代碼我們在每次使用user和token時都需要導入對應的Context,並且需要使用兩次useContext;

import React, { useContext } from 'react'
import { UserContext, TokenContext } from '../App'

export default function CustomHookContextDemo() {
  const user = useContext(UserContext);
  const token = useContext(TokenContext);

  console.log(user, token);

  return (
    <div>
      <h2>CustomHookContextDemo</h2>
    </div>
  )
}

我們可以抽取到一個自定義Hook中:

function useUserToken() {
  const user = useContext(UserContext);
  const token = useContext(TokenContext);

  return [user, token];
}

獲取窗口滾動的位置

在開發中,某些場景我們可能總是希望獲取創建滾動的位置:

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

export default function CustomScrollPositionHook() {

  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY);
    }
    document.addEventListener("scroll", handleScroll);

    return () => {
      document.removeEventListener("scroll", handleScroll);
    }
  }, [])

  return (
    <div style={{padding: "1000px 0"}}>
      <h2 style={{position: "fixed", top: 0, left: 0}}>CustomScrollPositionHook: {scrollPosition}</h2>
    </div>
  )
}

但是如果每一個組件都有對應這樣的一個邏輯,那麼就會存在很多的冗餘代碼:

function useScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const handleScroll = () => {
      setScrollPosition(window.scrollY);
    }
    document.addEventListener("scroll", handleScroll);

    return () => {
      document.removeEventListener("scroll", handleScroll);
    }
  }, [])

  return scrollPosition;
}

數據存儲的localStorage

在開發中,我們會有一些數據希望通過localStorage進行存儲(當然,你可以根據自己的情況選擇sessionStorage)

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

export default function CustomDataStoreHook() {
  const [name, setName] = useState(() => {
    return JSON.parse(window.localStorage.getItem("name"))
  });

  useEffect(() => {
    window.localStorage.setItem("name", JSON.stringify(name));
  }, [name])

  return (
    <div>
      <h2>CustomDataStoreHook: {name}</h2>
      <button onClick={e => setName("coderwhy")}>設置name</button>
    </div>
  )
}

如果每一個裏面都有這樣的邏輯,那麼代碼就會變得非常冗餘:

function useLocalStorange(key) {
  const [data, setData] = useState(() => {
    return JSON.parse(window.localStorage.getItem(key))
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(data));
  }, [data]);

  return [data, setData];
}

三. Hook原理分析

這裏我主要分析一下useState的原理,因爲本次教程是穿插講解源碼的,所以不會所有源碼一一講解。

3.1. useState代碼位置

useState還是從React中導入的,所以我們可以先查看:

image-20200729145143429

點到useState的源碼中:

  • useState本質上,是使用的dispatcher的useState;

image-20200729145217786

dispatcher來自另外的一個函數 resolveDispatcher

image-20200729145350575

運行的過程中,會賦值一個current的值是一個Dispatcher類型:

  • Dispatcher來自於 react-reconciler/src/ReactFiberHooks
image-20200729145507410

Dispatch類型的定義:

Dispatch類型的定義

這裏的Dispatch僅僅是一個類型而已,我們賦值具體的值在不同的階段是不同的:

  • 在掛載階段:HooksDispatcherOnMount

  • 在更新階段:HooksDispatcherOnUpdate

image-20200729150204035

掛載哪一個取決於renderWithHook函數:

image-20200729151949009

3.2. HooksDispatcherOnMount

HooksDispatcherOnMount對應的useState是mountState

image-20200729152902434

mountState的源碼:

mountState的源碼

綁定的dispatchAction函數,事實上是將所有的action放到了queue的隊列中:

dispatchAction函數

3.3. HooksDispatcherOnUpdate

HooksDispatcherOnUpdate對應的useState是updateState

image-20200729161113111

updateState本質上會執行updateReducer:

  • 所有其實就更新階段而言,useState本質上用的是updateReducer

image-20200729161236197

updateReducer的源碼如下:

image-20200729161529802
image-20200729161941808

四. Redux Hooks

在之前的redux開發中,爲了讓組件和redux結合起來,我們使用了react-redux中的connect:

  • 但是這種方式必須使用 高階函數 結合返回的 高階組件
  • 並且必須編寫: mapStateToPropsmapDispatchToProps 映射的函數;

在Redux7.1開始,提供了Hook的方式,我們再也不需要編寫connect以及對應的映射函數了

4.1. useSelector使用

useSelector的作用是將state映射到組件中:

  • 參數一:將state映射到需要的數據中;

  • 參數二:可以進行比較來決定是否組件重新渲染;(後續講解)

const result: any = useSelector(selector: Function, equalityFn?: Function)

現在,我可以改進一下之前的Profile中使用redux的代碼:

function Profile(props) {
  const {banners, recommends, counter} = useSelector(state => ({
    banners: state.homeInfo.banners,
    recommends: state.homeInfo.recommends
  }));

  console.log("Profile重新渲染");

  return (
    <div>
      <h2>數字: {counter}</h2>
      <h1>Banners</h1>
      <ul>
        {
          banners.map((item, index) => {
            return <li key={item.acm}>{item.title}</li>
          })
        }
      </ul>
      <h1>Recommends</h1>
      <ul>
        {
          recommends.map((item, index) => {
            return <li key={item.acm}>{item.title}</li>
          })
        }
      </ul>
    </div>
  )
}

但是這段代碼會有一個問題:

  • 當前我們的組件並不依賴counter,但是counter發生改變時,依然會引起Profile的重新渲染;

原因是什麼呢?

  • useSelector默認會比較我們返回的兩個對象是否相等;

  • 如何比較呢? const refEquality = (a, b) => a === b
  • 也就是我們必須返回兩個完全相等的對象纔可以不引起重新渲染;

這個時候,我們可以使用react-redux中給我們提供的 shallowEqual:

  • 這段代碼的作用是避免不必要的重新渲染;

  const {banners, recommends, counter} = useSelector(state => ({
    banners: state.homeInfo.banners,
    recommends: state.homeInfo.recommends
  }), shallowEqual);

當然,你也可以編寫自己的比較函數,來決定是否重新渲染。

4.2. useDispatch

useDispatch非常簡單,就是直接獲取dispatch函數,之後在組件中直接使用即可:

const dispatch = useDispatch()

直接使用dispatch:

<button onClick={e => dispatch(subAction(1))}>-1</button>
<button onClick={e => dispatch(subAction(5))}>-5</button>

我們還可以通過useStore來獲取當前的store對象:

const store = useStore()

在組件中可以使用store:

const store = useStore();
console.log(store.getState());
相關文章