用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback

本文由图雀社区成员 mRc 写作而成,欢迎加入图雀社区,一起创作精彩的免费技术教程,予力编程行业开展。

假设您觉得咱们写得还不错,记得 点赞 + 关注 + 评论 三连,鼓励咱们写出更好的教程

在第二篇教程中,咱们将手把手带y 1 – t c v你用自界说 Hook 重构之前的组件代码,让它变得更清晰、并且能够完成逻辑复用。在重构完成之后,咱们堕入了组件“不断获取数据并从头烘托”的无限循环,这时分,useCallback 站了出来,好像定海神针一般拯救了咱们的运用……

欢迎拜访本项意图 GitHL h U qub 库房和 Gitee 库房。

自界说 Hook:t ! H量身定制

在上一篇教程中,咱们经过动画的方法不断深入 useStateuseEffect,基本上理清了 React Hooks 背面的完J 6 _ O d c Q c成机制——链表,同时也完成了 COVID-19 数据可视化运用的全球数据总览和多个国家数据的直方图。

假设你想直接从这一篇教s Q U t A 4 A程开始阅读和实践,可下载本教程– * ) – /的源码:

git clone -b secondC r 6-part https:o f 9 t |//github.com/tuture-dev/covid-19-wit{ : r I 2h-hooks.git
# 或许克隆 Gitee 的库房
git clone -b second-partj v N a 6 E httpsI C U://gitee.com/tuture/covid-19-with-hooks.git

自界说 Hook 是 Real ] d * P V &ct Hooks 中最风趣的功用,# l F ( ( D @或许说特征。简略来说,它用一种高度灵活的方法,能够让你在不同的函数组件之间同享某些特定的逻辑。咱们先来经过一个十分简略的比如来看一下。

一个简略的自界说 Hook

先来看一个 Hook,名为 useBodyScrollPosition ,用于获取当时浏览器的垂直翻滚方位:

function useBodyScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(null);
useEffect(() => {
const handleScroll = () => setScrollPosition(window.scr& 4 ] X . oollY);
document.addEventListener('scroll', handleScroll);
return () =&gC ? 0 * _ 9 d p ?t;
do9 L | q w ncument.removeEventListener('scroll', handleScroll);
}, []);
return scrollPosition;K P o
}

经过调查,咱们能够发现自界说 Hook 具有以下f j L 1 = J s )特点:

  • 表面上:Y k 0 % E 6一个命名格局为 useXXX 的函数,但不是 Reac s % Yt 函数式组件
  • 本质上:内部经过运用t l + O Y F F L React 自带的一些l z , y . T f 2 b Hook (例如 useStateuseEffec7 M { 6 v V . ^t )来完成某些通用的逻辑

D D , Q }设你发散一下思维,能够想到有许多当地能够去做自界说 Hook:DOM 副E ] A作用修正/监听、动画、恳求、表单操作、数据存储等等。

2 z N $ M J J H v

这儿引荐两个强壮的 React Hooks 库:React Use 和 Umi Hooks。它们都完成了p – K许多出产级别的自界说 HoW ~ Z % ; Cok,十分值得学习。

我想这便是 React Hooks 最大的魅力——经过几个内置的 Hook,你能够依照某些约好进行任意组合,“制造出”任7 D P何你真实需求的 Hook,或许调用别人写好的 Hook,然后轻松应对各种复杂的事务场景。就好像大千世界无奇不有,却不过是由一百多种元素组合而成。

管窥自界说 Hook 背面的原理

又到了动画时刻。咱们^ – ( k R x ) , tH b g l看看在组件初度烘托时的景象:

用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback

咱们在 App 组件中调用了 useCustomHook 钩子。能够看到,即使咱们切换到了自界说 Hook 中,Hook 链表的生成依旧没有改动。再来看垂青烘托的状况:

用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback

相同地,即使代码的履行进入到自界说 Hook 中,咱们仍然能够从 Hook 链表中读取到相应的数据,这个”配对“的进程O & m 4 4 P 4 3总能成功。

咱们再次回味一下 Rules of Hook。它规则只有在两个当地能够运用 R] q 8 Keact Hook:

  1. React 函数组件
  2. 自界说 Hook

第一点咱们早就清楚了,第二点经过方才的两个动画相信你也明白了:自界说 Hook 本质上仅仅把调用内置 Hook 的进程封装成一个个能够复用7 O n的函数,并不影响 Hook 链表的生! d Y Q 4成和读取

实战环节

让咱们持续 COVID-19 数据运用的开发。接下来,咱们计划完成历史数据的展现,– r N m包含确诊病例、逝世病例和治好人数。

咱们首先来完成一个自界说 Hook,名为 useCoronaAPI ,用于同享从 NovelCOVID 19 API 获取数据的逻辑。创立 src/hooks/useCoronaAPI.jw . ` 7s,填写代码如下:

import { useState, useEffectD V H | m ; ~ O } from "react";
consi g e j Ct BASE_URL = "https://corona.lmao.ninja";
export funcT e ] S V k q K ?tion useCoronaAPI(
path,
{ initialData = null, converter = (da+ n z ~ k O b } 5ta) => data, reW L M 8fetchIntervaY k gl = null }
) {
const [data, setData] = useState(initialData);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(`${BASE_URL}${path}`);
const data = await response.json();C I [
setData(converter(data));
};
fetchData();
if (refetchInterval) {
const intervalId = setIntervalK 3 9 T F | c c P(fetchData, refetchInterval);
return () => clearInterval(intervalId);
}
}, [_ H Q ^ k / ; 2 )converter, path, refetchInterval]);
return data;
}

能够看到,界说的 useCoronaAPI 包含两个参数,第一个是 path ,也便是 API 途T l 6 ! & + `径;第二是装备参数,包含以下参数:

  • initialData :初始为空的默许数据
  • converter :对原始数据的转换函数(默许是一个恒等函数)
  • refetchInterval :从头3 b 2 n 7 ) : ! !获取数据的距离(以毫秒为单位)

此外,咱们还~ G 5 C要留意 useEffectB ~ + ? { $传入的 deps 数组,包含了三个元素(都是在 Effect 函数中用到的):conver; ; K j l .terpathrefetchInterval ,均来自 useCoronaAPI 传入的参数。

提示

在上一篇文章中,咱们简% F I c y略地提到过,不要对 useEffect 的依靠说谎,那么这儿便是一个t | ^很好的事例:咱们将 Effect 函数所有用到的外部数据(包含函数)全部加入到了依靠数组中。当然,由于 BASE_URL 归于模块级别的常量,因而不需求作为依靠。! U m W ]过这儿留了个坑,嘿嘿# c x – C Y $……

然后在根组件 src/App.js 中运用刚刚创立的 useCoronai $ b oAPI6 u ; c 0 [ ? # D子,代码如下:

import React8 5 9 B s, { useState } from "react";
// ...
import { useCoronaAPI } from "./hooks/useCoronaAPI";
function AppO Q c W ( y b() {
const globalStatsf s ? 8 : q = useCoronaAPI("/all", {
initialData: {},
refetchInterval: 5000,
});
const [key, setKey] = useState("cases");
const countries = useCoronaAPI(`/countries?sort=${key}`, {
initialData: [],
converter: (data) => data.slice(0, 10),
});
r_ ~ ] k P Leturn (
// ...
);a I ( R 
}
e_ 9 G k f _ * 0xport default App;

整个 App 组件变得清晰了许多,不是吗?

可是当咱们满怀期待地把运用跑起来,却发现整个运用堕入“无( 2 T 2 o 9 o 0 $限恳求”的# $ 9 0 w怪圈中。翻开 Chrome 开发者工具的 Network 选项卡,你会发现网络恳求数量始终在飙升……

用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback

吓得咱们赶紧把网页关了。冷静下/ @ 5 u来之后,不由沉思:这到底是为什么呢?

危险

Nove_ a 6 + 4lCOVID 19 API 归于公益性质的数据资源,咱们应该尽快把页面关掉,避免给. k Q { y G !对方的服务器造成太大的恳求压力。

useCall N m ! c c o u blback:定海神针

假设你一字一句把上一篇文章看下来,其实或许现已发现了问题的头绪:

依靠数组在判别元素是否发作改动时运用了Object.is进行比较,因而当deps中某一元素为非原始类型时(例如函数、目标等),每次烘托都会发作改动,然后每次都会触发 Effect,失去了deps本身的意义。

OK,假设你; o ; 7 r 5 v Z没有印象也没联系,咱们先来聊一聊初学 Reau _ { Bct Hooks 经常会遇到的一个问题:Effect 无限循环。

关于 Effect 无限9 t k m循环

来看一下这段”永不中止“的计数器:

function Endless: ? Q u G h )Counter() {
const [count, sez 9 d . :tCount] = useState(0);
useEffect(() => {
setTimeout(() => setCount(count + 1F p o = | S . 4 0), 1000);
});
return (
<div clasR 0 Z 0 ? qsName="App">
<h1>{count}</h1>
<0 i _ 2/div>
);
}

假设你去运行这段代码,会发现T $数字永久在] o V M x增长。咱们来经过一段动画来演示一下这个”无限循环“到底是怎样回事:

用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback

咱们的组件堕入了:烘托 => 触发 EffeD c x G t i | :ct => 修正状$ u d W .态 => 触发重烘托的无限循环。

想必你现已发现 useEffect 堕入无限循环的”罪魁祸首“了——由于没有提供正确的 dG p k b $ o v | Aeps !然后导致每次烘托后都会去履行 Effect 函数。事实上,在之前的 useCoronaAPI 中,也是由于传入的 deps 存在问题,导致每次烘托后都去履行 Effect 函数去获取数据,堕入了无限循环。那么,到底是哪个依靠出现了x = i – a ?问题?

没错,便是那个 converter 函数!咱们知道,在 JavaScript 中,原始类型和非原始类型在判别值是否相同的时分有巨大的不同:

// 原始类型
true === true // true
1 === 1 // true
'a' === 'a' // true
// 非原始类型
{} === {} // false
[]V / Y M 6 E a X == ; 4 ` `  . z= [] // false
() => {G P 7 , S 1 W . d} ==_ X = S J g U= () => {} // false

相同,每次传入的 converter 函数尽管形式上相同,但仍然是不同的函数(引证不持平),然后导致每次都会履行 Effect 函数。

关于回忆化缓存(Memoization)

Memoization,一般称为回忆化缓存(或许“回忆”),听上去是很深邃的核算机专业术语,可是它背面的思维很简略:假设咱们有一个核算量很大的纯函数(给定相同的输入,一定会得到相同的输出),那么咱们在第一次遇到特定输入的时分,把它的输出成果“记”(缓存)下来,那么下次碰到相同的输出,只需求从缓存里面拿出来直接回来就能够了,省去了核算的进程!

实际上,除了节省不必要的核算、然后进步程序功能之外,Memoization 还有一个用处:用了保证回来值的引证持平

咱们先经过一段简略的求平方根的函数,了解一下 Memoization 的原理。首先^ L y + l { D是一个没有缓存的版别:

function sqrd k tt(arg) {
return { result: Math.sqrt(arg) };
}

你也许留意到了咱们特地回来了一个目标来记录成果,咱们后面会和 Memoized 的版别进行对比分析。然后是加了缓存的版别:

function memoizedSqrt(arg) {
// 假设 cache 不存在,则初始化一个空目标
if (!memoizedSqrt.cache) {
memoizedSqrt.cache = {};
}
// 假设 ce 0 H ache 没有命中,则先核算,再存入 cache,然后回来成果
if (!memoizedSqrt.cache[arg]) {
return memoizedSqrt.cache[arg] = {6 2 s result: Math.sqrt(arg)z 9 9 k D 1 U };
}
// 直接回来 cache 内的成果,无需核算
return memoizedSqrt.cache[arg];
}

然后咱们尝试调用这两个函数,就会发现一些显着的区别:

sqrt(9)                      // { result: 3 }
sn W d Cqrt(9) === sqrt(9)          // false
Object.is(sqrt(9), sqrt(9))  // false
memoizedU % : 0 !Sqrt(9)                              // { result: 3 }
memoizedSqrt(9) === memoizedSqrt(9)          // true
Object.P o N dis(memoizedSqrt(9), memoizedSqrt(9))  // true

一般的 sqrt 每次回来的成果的引证都不相同(或许说是一个全新的目标),而 memoizedSqrt 则能$ 1 ^ Q * w y J回来完全相同的目标。因而在 React 中,经过 Memoization 能够保证多次烘托中的 Prop 或许状态的引证持平,然后能够避免不必要的重烘托或许副作用履行。

让咱们来总结一下回忆化缓存(Memoization)的两个运用场景:

  • 经过缓存核算成果,节省费时的核算
  • 保证相同输入下回来} c 0 P r .值的引证持平

运用方法和原理解析

为了处理函数在多次烘托中的引证持平(Referential Equality)问题,c – T g n RReact 引入了一个重要的 Hook—— useCallbacY K u 5 6k。官方文档介绍的运用方法如下:

const memoizedCallback = useCallback(callback, deps);

第一个参数 callback 便是需求回忆的函数,第二个参数便是咱们了解的 deps 参数,相同也是一个依靠数组(有时分也被称为输入 inputs)。在 Memoizati 4 + ] @ % Don 的W { s上下文中,这个 deps 的作用适当于缓存中的键(Key),假设键没有改动,那么就直接回来缓存中的函数,并且保证是引证相同的函数。

在大多数状况下,咱们都是传入空数组 [] 作为 deps 参数,这样 useCallback 回来的就始终是同一个函数,永久不/ % U K 5 C y ` @会更新

提示

你也许在刚开始学习 us8 ( TeL Q P p 5 # ; ) GEffect 的时分就发现:咱们并不需求把 useState 回来的 r K [ G V第二个 Setter 函数作为 Effect 的依靠。实际上,React 内部现已对 Setter 函数做了 Memoization 处理,因而每次烘托拿到的 Setter 函数都是完全相同的,deps 加不加都是没有影响的。

依照n q O h L +常规,咱们还是经过一段动画来了解一下 useCallback 的原理(depsC n + 6 q 1 w Q Z 为空数组的状况),首先是初度烘托:

用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback

和之前相同,调用 useCallback 也是追加到 Hook 链表上,不过这儿着重强调了这个函数 f1 所指向的内存方位(随意画了一个),然后明确告诉咱们:这个 f1 始终是指向同一个函数。然! O q b v e 8 u后回来的 onClick 则是指向 Hook 中存储的 f1

再来看垂青烘托的状况:

用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback

重烘托的时分,再次调用 useCallback 相同回来给咱们 f1 函数,并且这个函数还是指向同一块内存,然后使得 onClick 函数和上次烘~ d !托时真实做到了I $ ] N证持平

useCallback 和 useMemo 的联系

咱们知道 useCallback 有个好基友叫 usew 7 : m #Me7 9 2 p I z ] 7mo。还记得咱们之前总结了 Memoization 的两大场景吗?useCalB @ K w v ! e ylback 主要是为了处理函数的”引证持平“问题,而 useMemo 则是一个”全能型选手“,能够同时胜任引证持平和节省核算的使命。

实际上,useMemo 的功用是 useCallback超集。与 useCallback 只能缓存函4 |数比较,useMemo 能够缓存任何类型的值(当然也包含函数)。useMemo 的运用方法如下:

const memoizedValue = useMemo(() =>c J @ - Y O computeExpensiveValue(a, b), [a, b]);

其间第一个c 1 = o 3 i Y参数是一个函2 . N 3 # H N } #数,这个函数回来值的回来值(也便是上I ~ Lcomf H ~puteExpensiveValue 的成果)将回来给 memoizedValue 。因而以下两个钩子的运用是完全等价的:

useCalZ T i [ h n &lback(fn, deps);
useMemo(() => fn, dM j [ 9 9eps);

鉴于在前端开发中遇到的核算密集型使命是适当少的,并且浏览器引擎6 l B的功能也足够8 { z O U优异,因而这一系列文章不会深入去解说 useMemo 的运用。更何况,现已把握/ A O | V U useCallback 的你,应该也现已知道怎样去运用 useMemo 了吧?

实战环节

了解了 useCallback 之后,咱们开始修复 useCoronaAPI 钩子的问题。修k @ s A k 9src/hooks/useCoronaAPI ,代码如2 C . a 1 1 ;下:

import { useState, useEx X ( f ? f (ffect, useCallback } from "react";
// ...
export function useCoronaAPI(. ^ f _ 2 I ( x 8
/{ ; A _ K 5 + f e/ ...
) {
const [data, setData] = useState(initz D # e !ialData);
const convertDataN b Z $ 5 = useCallback(converter, []);
useEffect(() => {
const f_ 0 x @etchData = async () => {
// ..! C O Z + ! 0 H X.
setData(convertData(data));
};
fetchData();
// ...
}, [convertData, path, refetchInterval]);
return data;
}

能够看到,咱们把 converter 函数+ q ;useCallback 包裹了起来,把回忆化处理后的函数命名为 convertData,并且传入的 deps 参数为空数组 [] ,保证每次烘托都相同。然后把 useEffect 中所有的 converter 函数相应修正成 convertData

最终^ o j % 2 [ p O再次开启项目,一切又回归了正常,这次自界说 Hook 重构圆满完成!鄙人一篇教程中,咱们将开始进一步推动Z r t COVID-19 数据可视化项意图推动,经过曲线G % K v g + c E f图的方法完成历史数据的展现(包含确诊、逝世和治@ P A好)。数据状态变得越来越复杂,咱们又该怎么应n B Q l ? ; U o对呢?敬请期待。

剧透提示:用 useReducer + useCo% ) o B w z 5 jntext 完成一个简略的 Redux!

参考资料

  • React 官方文档
  • DT-FE:怎样用 React Hooks 造轮子
  • WellPaidGeed:How to write custom hooks in React
  • Netlify Blog:Deep dive: Ho? ( Iw do Reacs @ a n v W h U *t hooks really work?
  • Andrew Myint:v A 0How to Fix the Infinit! o Ze Loop Inside “usek 5 S 0 C D ^ XEffect” (React Hooks)
  • Kent C. Dodds:When to useMemo and use{ = i C + c pCallback
  • Sandro Dolidze:React Hooks: Memoization
  • Chidume Nnamdi:Ut & k # D D snderstanding Memoization in JavaScript to Improve Performance

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback