本文由圖雀社區成員 mRc 寫做而成,歡迎加入圖雀社區,一塊兒創做精彩的免費技術教程,予力編程行業發展。javascript
若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連,鼓勵咱們寫出更好的教程💪html
在第二篇教程中,咱們將手把手帶你用自定義 Hook 重構以前的組件代碼,讓它變得更清晰、而且能夠實現邏輯複用。在重構完成以後,咱們陷入了組件「不斷獲取數據並從新渲染」的無限循環,這時候,useCallback 站了出來,如同定海神針通常拯救了咱們的應用……前端
在上一篇教程中,咱們經過動畫的方式不斷深刻 useState
和 useEffect
,基本上理清了 React Hooks 背後的實現機制——鏈表,同時也實現了 COVID-19 數據可視化應用的全球數據總覽和多個國家數據的直方圖。react
若是你想直接從這一篇教程開始閱讀和實踐,可下載本教程的源碼:git
git clone -b second-part https://github.com/tuture-dev/covid-19-with-hooks.git
# 或者克隆 Gitee 的倉庫
git clone -b second-part https://gitee.com/tuture/covid-19-with-hooks.git
複製代碼
自定義 Hook 是 React Hooks 中最有趣的功能,或者說特點。簡單來講,它用一種高度靈活的方式,可以讓你在不一樣的函數組件之間共享某些特定的邏輯。咱們先來經過一個很是簡單的例子來看一下。github
先來看一個 Hook,名爲 useBodyScrollPosition
,用於獲取當前瀏覽器的垂直滾動位置:編程
function useBodyScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(null);
useEffect(() => {
const handleScroll = () => setScrollPosition(window.scrollY);
document.addEventListener('scroll', handleScroll);
return () =>
document.removeEventListener('scroll', handleScroll);
}, []);
return scrollPosition;
}
複製代碼
經過觀察,咱們能夠發現自定義 Hook 具備如下特色:json
useXXX
的函數,但不是 React 函數式組件useState
和 useEffect
)來實現某些通用的邏輯若是你發散一下思惟,能夠想到有不少地方能夠去作自定義 Hook:DOM 反作用修改/監聽、動畫、請求、表單操做、數據存儲等等。數組
提示
這裏推薦兩個強大的 React Hooks 庫:React Use 和 Umi Hooks。它們都實現了不少生產級別的自定義 Hook,很是值得學習。
我想這即是 React Hooks 最大的魅力——經過幾個內置的 Hook,你能夠按照某些約定進行任意組合,「製造出」任何你真正須要的 Hook,或者調用他人寫好的 Hook,從而輕鬆應對各類複雜的業務場景。就好像大千世界無奇不有,卻不過是由一百多種元素組合而成。
又到了動畫時間。咱們來看看在組件初次渲染時的情形:
咱們在 App
組件中調用了 useCustomHook
鉤子。能夠看到,即使咱們切換到了自定義 Hook 中,Hook 鏈表的生成依舊沒有改變。再來看看重渲染的狀況:
一樣地,即使代碼的執行進入到自定義 Hook 中,咱們依然能夠從 Hook 鏈表中讀取到相應的數據,這個」配對「的過程總能成功。
咱們再次回味一下 Rules of Hook。它規定只有在兩個地方可以使用 React Hook:
第一點咱們早就清楚了,第二點經過剛纔的兩個動畫相信你也明白了:自定義 Hook 本質上只是把調用內置 Hook 的過程封裝成一個個能夠複用的函數,並不影響 Hook 鏈表的生成和讀取。
讓咱們繼續 COVID-19 數據應用的開發。接下來,咱們打算實現歷史數據的展現,包括確診病例、死亡病例和治癒人數。
咱們首先來實現一個自定義 Hook,名爲 useCoronaAPI
,用於共享從 NovelCOVID 19 API 獲取數據的邏輯。建立 src/hooks/useCoronaAPI.js
,填寫代碼以下:
import { useState, useEffect } from "react";
const BASE_URL = "https://corona.lmao.ninja";
export function useCoronaAPI( path, { initialData = null, converter = (data) => data, refetchInterval = null } ) {
const [data, setData] = useState(initialData);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(`${BASE_URL}${path}`);
const data = await response.json();
setData(converter(data));
};
fetchData();
if (refetchInterval) {
const intervalId = setInterval(fetchData, refetchInterval);
return () => clearInterval(intervalId);
}
}, [converter, path, refetchInterval]);
return data;
}
複製代碼
能夠看到,定義的 useCoronaAPI
包含兩個參數,第一個是 path
,也就是 API 路徑;第二是配置參數,包括如下參數:
initialData
:初始爲空的默認數據converter
:對原始數據的轉換函數(默認是一個恆等函數)refetchInterval
:從新獲取數據的間隔(以毫秒爲單位)此外,咱們還要注意 useEffect
所傳入的 deps
數組,包括了三個元素(都是在 Effect 函數中用到的):converter
、path
和 refetchInterval
,均來自 useCoronaAPI
傳入的參數。
提示
在上一篇文章中,咱們簡單地提到過,不要對
useEffect
的依賴說謊,那麼這裏就是一個很好的案例:咱們將 Effect 函數全部用到的外部數據(包括函數)所有加入到了依賴數組中。固然,因爲BASE_URL
屬於模塊級別的常量,所以不須要做爲依賴。不過這裏留了個坑,嘿嘿……
而後在根組件 src/App.js
中使用剛剛建立的 useCoronaAPI
鉤子,代碼以下:
import React, { useState } from "react";
// ...
import { useCoronaAPI } from "./hooks/useCoronaAPI";
function App() {
const globalStats = useCoronaAPI("/all", {
initialData: {},
refetchInterval: 5000,
});
const [key, setKey] = useState("cases");
const countries = useCoronaAPI(`/countries?sort=${key}`, {
initialData: [],
converter: (data) => data.slice(0, 10),
});
return (
// ...
);
}
export default App;
複製代碼
整個 App
組件變得清晰了不少,不是嗎?
可是當咱們滿懷期待地把應用跑起來,卻發現整個應用陷入「無限請求」的怪圈中。打開 Chrome 開發者工具的 Network 選項卡,你會發現網絡請求數量始終在飆升……
嚇得咱們趕忙把網頁關了。冷靜下來以後,不由沉思:這究竟是爲何呢?
危險
NovelCOVID 19 API 屬於公益性質的數據資源,咱們應該儘快把頁面關掉,避免給對方的服務器形成太大的請求壓力。
若是你一字一句把上一篇文章看下來,其實可能已經發現了問題的線索:
依賴數組在判斷元素是否發生改變時使用了
Object.is
進行比較,所以當deps
中某一元素爲非原始類型時(例如函數、對象等),每次渲染都會發生改變,從而每次都會觸發 Effect,失去了deps
自己的意義。
OK,若是你沒有印象也不要緊,咱們先來聊一聊初學 React Hooks 常常會遇到的一個問題:Effect 無限循環。
來看一下這段」永不中止「的計數器:
function EndlessCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => setCount(count + 1), 1000);
});
return (
<div className="App"> <h1>{count}</h1> </div>
);
}
複製代碼
若是你去運行這段代碼,會發現數字永遠在增加。咱們來經過一段動畫來演示一下這個」無限循環「究竟是怎麼回事:
咱們的組件陷入了:渲染 => 觸發 Effect => 修改狀態 => 觸發重渲染的無限循環。
想必你已經發現 useEffect
陷入無限循環的」罪魁禍首「了——由於沒有提供正確的 deps
!從而致使每次渲染後都會去執行 Effect 函數。事實上,在以前的 useCoronaAPI
中,也是由於傳入的 deps
存在問題,致使每次渲染後都去執行 Effect 函數去獲取數據,陷入了無限循環。那麼,究竟是哪一個依賴出現了問題?
沒錯,就是那個 converter
函數!咱們知道,在 JavaScript 中,原始類型和非原始類型在判斷值是否相同的時候有巨大的差異:
// 原始類型
true === true // true
1 === 1 // true
'a' === 'a' // true
// 非原始類型
{} === {} // false
[] === [] // false
() => {} === () => {} // false
複製代碼
一樣,每次傳入的 converter
函數雖然形式上同樣,但仍然是不一樣的函數(引用不相等),從而致使每次都會執行 Effect 函數。
Memoization,通常稱爲記憶化緩存(或者「記憶」),聽上去是很高深的計算機專業術語,可是它背後的思想很簡單:假如咱們有一個計算量很大的純函數(給定相同的輸入,必定會獲得相同的輸出),那麼咱們在第一次遇到特定輸入的時候,把它的輸出結果「記」(緩存)下來,那麼下次碰到一樣的輸出,只須要從緩存裏面拿出來直接返回就能夠了,省去了計算的過程!
實際上,除了節省沒必要要的計算、從而提升程序性能以外,Memoization 還有一個用途:用了保證返回值的引用相等。
咱們先經過一段簡單的求平方根的函數,熟悉一下 Memoization 的原理。首先是一個沒有緩存的版本:
function sqrt(arg) {
return { result: Math.sqrt(arg) };
}
複製代碼
你也許注意到了咱們特意返回了一個對象來記錄結果,咱們後面會和 Memoized 的版本進行對比分析。而後是加了緩存的版本:
function memoizedSqrt(arg) {
// 若是 cache 不存在,則初始化一個空對象
if (!memoizedSqrt.cache) {
memoizedSqrt.cache = {};
}
// 若是 cache 沒有命中,則先計算,再存入 cache,而後返回結果
if (!memoizedSqrt.cache[arg]) {
return memoizedSqrt.cache[arg] = { result: Math.sqrt(arg) };
}
// 直接返回 cache 內的結果,無需計算
return memoizedSqrt.cache[arg];
}
複製代碼
而後咱們嘗試調用這兩個函數,就會發現一些明顯的區別:
sqrt(9) // { result: 3 }
sqrt(9) === sqrt(9) // false
Object.is(sqrt(9), sqrt(9)) // false
memoizedSqrt(9) // { result: 3 }
memoizedSqrt(9) === memoizedSqrt(9) // true
Object.is(memoizedSqrt(9), memoizedSqrt(9)) // true
複製代碼
普通的 sqrt
每次返回的結果的引用都不相同(或者說是一個全新的對象),而 memoizedSqrt
則能返回徹底相同的對象。所以在 React 中,經過 Memoization 能夠確保屢次渲染中的 Prop 或者狀態的引用相等,從而可以避免沒必要要的重渲染或者反作用執行。
讓咱們來總結一下記憶化緩存(Memoization)的兩個使用場景:
爲了解決函數在屢次渲染中的引用相等(Referential Equality)問題,React 引入了一個重要的 Hook—— useCallback
。官方文檔介紹的使用方法以下:
const memoizedCallback = useCallback(callback, deps);
複製代碼
第一個參數 callback
就是須要記憶的函數,第二個參數就是你們熟悉的 deps
參數,一樣也是一個依賴數組(有時候也被稱爲輸入 inputs
)。在 Memoization 的上下文中,這個 deps
的做用至關於緩存中的鍵(Key),若是鍵沒有改變,那麼就直接返回緩存中的函數,而且確保是引用相同的函數。
在大多數狀況下,咱們都是傳入空數組 []
做爲 deps
參數,這樣 useCallback
返回的就始終是同一個函數,永遠不會更新。
提示
你也許在剛開始學習
useEffect
的時候就發現:咱們並不須要把useState
返回的第二個 Setter 函數做爲 Effect 的依賴。實際上,React 內部已經對 Setter 函數作了 Memoization 處理,所以每次渲染拿到的 Setter 函數都是徹底同樣的,deps
加不加都是沒有影響的。
按照慣例,咱們仍是經過一段動畫來了解一下 useCallback
的原理(deps
爲空數組的狀況),首先是初次渲染:
和以前同樣,調用 useCallback
也是追加到 Hook 鏈表上,不過這裏着重強調了這個函數 f1
所指向的內存位置(隨便畫了一個),從而明確告訴咱們:這個 f1
始終是指向同一個函數。而後返回的 onClick
則是指向 Hook 中存儲的 f1
。
再來看看重渲染的狀況:
重渲染的時候,再次調用 useCallback
一樣返回給咱們 f1
函數,而且這個函數仍是指向同一塊內存,從而使得 onClick
函數和上次渲染時真正作到了引用相等。
咱們知道 useCallback
有個好基友叫 useMemo
。還記得咱們以前總結了 Memoization 的兩大場景嗎?useCallback
主要是爲了解決函數的」引用相等「問題,而 useMemo
則是一個」全能型選手「,可以同時勝任引用相等和節約計算的任務。
實際上,useMemo
的功能是 useCallback
的超集。與 useCallback
只能緩存函數相比,useMemo
能夠緩存任何類型的值(固然也包括函數)。useMemo
的使用方法以下:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼
其中第一個參數是一個函數,這個函數返回值的返回值(也就是上面 computeExpensiveValue
的結果)將返回給 memoizedValue
。所以如下兩個鉤子的使用是徹底等價的:
useCallback(fn, deps);
useMemo(() => fn, deps);
複製代碼
鑑於在前端開發中遇到的計算密集型任務是至關少的,並且瀏覽器引擎的性能也足夠優秀,所以這一系列文章不會深刻去講解 useMemo
的使用。更況且,已經掌握 useCallback
的你,應該也已經知道怎麼去使用 useMemo
了吧?
熟悉了 useCallback
以後,咱們開始修復 useCoronaAPI
鉤子的問題。修改 src/hooks/useCoronaAPI
,代碼以下:
import { useState, useEffect, useCallback } from "react";
// ...
export function useCoronaAPI( // ... ) {
const [data, setData] = useState(initialData);
const convertData = useCallback(converter, []);
useEffect(() => {
const fetchData = async () => {
// ...
setData(convertData(data));
};
fetchData();
// ...
}, [convertData, path, refetchInterval]);
return data;
}
複製代碼
能夠看到,咱們把 converter
函數用 useCallback
包裹了起來,把記憶化處理後的函數命名爲 convertData
,而且傳入的 deps
參數爲空數組 []
,確保每次渲染都相同。而後把 useEffect
中全部的 converter
函數相應修改爲 convertData
。
最後再次開啓項目,一切又迴歸了正常,此次自定義 Hook 重構圓滿完成!在下一篇教程中,咱們將開始進一步推動 COVID-19 數據可視化項目的推動,經過曲線圖的方式實現歷史數據的展現(包括確診、死亡和治癒)。數據狀態變得愈來愈複雜,咱們又該如何應對呢?敬請期待。
劇透提醒:用 useReducer + useContext 實現一個簡單的 Redux!
想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。