都2022年了还不考虑来学React Hook吗?6k字带你从入门到吃透

1,500 阅读19分钟

「这是我参与2022首次更文挑战的第六天,活动详情查看:2022首次更文挑战」。

🧨 大家好,我是 Smooth,一名大二的 SCAU 前端er
🏆 文章会为你讲述 React 11种 Hook 的日常用法以及进阶操作,由浅入深带你彻底掌握 React Hook!
🙌 如文章有误,恳请评论区指正,谢谢!

React Hooks 是什么

React Hooks 是 React V16.8 中推出的新特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

React Hooks 的设计初衷,就是用于加强函数组件(原有函数组件无法存在状态),让你能够在不使用"类"的前提下写出一个全功能的组件。

顾名思义 Hooks 在中文中是“钩子”的意思,Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。

React Hooks 的特点

  1. 代码更清晰,解决了类组件难维护、逻辑不易复用的问题
  2. hooks 之间的状态是独立的,有自己独立的上下文,不会出现混淆状态的情况,解决了类组件中 this指针指向不明确或由于使用不当而造成不必要的错误
  3. 让函数有了状态管理
  4. 避免函数重复执行的副作用(通过相关 hook 进行限制)
  5. Hooks 可以引用其他 Hooks,通过组合可以更方便复用
  6. 更容易将组件的视图与状态分离

使用 Hook 的条件

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则:

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook,  确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook。 你可以:

  • ✅ 在 React 的函数组件中调用 Hook
  • ✅ 在自定义 Hook 中调用其他 Hook

遵循此规则,确保组件的状态逻辑在代码中清晰可见。

同时,React 官方提供了一个 eslint-plugin-react-hooks 插件来强制执行这些规则,不按规则使用直接给你进行警告或报错

React Hooks 的使用

重点来了!

1. useState

const [count, setCount] = useState(0)

给函数组件添加初始状态(变量初始化),它和 Class Component 中的 this.setState 类似。

当我们使用 useState 定义 state 变量时候,它返回一个有两个值的数组。第一个值是当前的 state,第二个值是更新 state 的函数 和闭包差不多,暴露函数内部的值和一个修改该值的函数,只不过要注意的是,该函数对数据的更新是异步的

下面是一个实现计数器的简易 demo

需求:点击 Click me,计数进行 + 1

image.png image.png

如图可知,useState 对 count 进行初始化为0,以及提供了一个可修改 count 值的函数 setCount,每次点击按钮都触发 onClick 事件调用 setCount 函数,在 count 的基础上进行 +1

所谓批量更新原则

熟悉 React 的同学都清楚所谓 state 的变化 React 内部遵循的是批量更新原则。

所谓异步批量是指在一次页面更新中如果涉及多次 state 修改时,会合并多次 state 修改的结果得到最终结果从而进行一次页面更新。

为什么 React Hook 采取批量更新原则?

官方解释:如果不在当前屏幕,我们可以延迟执行相关逻辑。如果数据数据到达的速度快过帧速,我们可以合并、批量更新。我们优先执行用户交互(例如按钮点击形成的动画)的工作,延后执行相对不那么重要的后台工作(例如渲染刚从网络上下载的新内容),从而避免掉帧。

关于如何辨别异步批量更新
  1. 凡是React可以管控的地方,他就是异步批量更新。比如事件函数,生命周期函数中,组件内部同步代码。

  2. 凡是React不能管控的地方,就是同步批量更新。比如setTimeout,setInterval,源生DOM事件中,包括Promise都是同步批量更新。

在 React 18 中通过 createRoot 中对外部事件处理程序进行批量处理,换句话说最新的 React 中关于 setTimeout、setInterval 等不能管控的地方都变为了批量更新。



2. useEffect

useEffect(() => { 
    //此处编写 组件挂载之后和组件重新渲染之后执行的代码 ... 
    
    return () => { 
        //此处编写 组件即将被卸载前执行的代码 ... 
    } 
}, [])

useEffect 被称为副作用钩子,这个 Hook 和 useState 一样是一个基础钩子。Effect Hook 可以让你在函数组件中执行副作用操作,修改了数据获取、设置订阅、手动更改 React 组件中的 DOM、console.log() 、ajax 操作等等都是副作用。同时,可以使用多个 Effect 实现关注点分离。

类似于 Vue 中的计算属性

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect 支持三个参数

  • 第一个参数为一个函数,表示副作用效应函数,默认情况下它在第一次渲染之后和每次更新之后都会执行。

  • 第二个参数是一个数组,指定了第一个参数(副效应函数)的依赖项。只有该数组中的变量发生变化时,副效应函数才会执行,如果数组为空,则代表该 useEffect 不随任何变量发生变化,即只渲染一次。

  • 第三个参数是 useEffect 的返回值,返回一个函数,在 useEffect 执行之前,都会先执行里面返回的函数,一般用于添加销毁事件

注意事项

  1. useEffect 是在 render(浏览器完成画面渲染) 之后才执行,所以该 Hook 可接受第二个参数来控制跳过执行,下次 render 后如果指定的值没有变化就不会执行。

  2. useEffect 监听某个特定值时,不能对其进行 setValue 否则就会陷入死循环,直到页面卡死。



3. useLayoutEffect

useLayoutEffect 与 useEffect 使用方式是完全一致的,useLayoutEffect 的区别在于它会在所有的DOM 变更之后同步调用 effect。

useEffect:执行时机在 render 之后
useLayoutEffect:执行时机在所有的 DOM 更新之后

可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前, useLayoutEffect 内部的更新计划将被同步刷新。

当然,有时你可能需要使用另外一个情况下,如果你要更新的值(像 ref ),此时你需要确保它是在最新的任何其他代码运行之前,此时可以考虑使用 useLayoutEffect ,而不是 useEffect,

通常对于一些通过 JS 计算的布局,如果你想减少 useEffect 带来的「页面抖动」,你可以考虑使用 useLayoutEffect 来代替它。



4. useContext

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

熟悉 React 中Context Api 和 Vue 中的 provide/inject Api 的同学可能会对这个钩子的作用深有体会。

如果还不是很能理解作用,请看以下 demo

image.png

在根组件上我们需要向下传递一个 count 属性给第三层子组件 H 使用

此时,如果使用 props 的方法进行层层传递那么无疑是一种噩梦。而且如果我们的 H 组件需要使用 count 但是 B、E 并不需要,如果使用 props 的方法难免在 B、E 组件内部也要显式声明 count

React 中正是为了解决这样的场景提出来 Context Api。

可以通过 React.createContext 创建 context 对象,在根组件中通过 Context.Provider 的 value 属性进行 count 变量的分发,从而在 Function Component 中使用 useContext(Context) 获取对应的值。

image.png

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

如果你想传递多个数据给子组件可以进行多个 Context.Provider 的嵌套



5. useReducer

React Hook 中还提供了一个关于状态管理的 useReducer,类似于 Vue 的 Vuex,全局状态管理工具

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

useReducer 接受三个参数

  • reducer 函数
  • 初始值 initialArg
  • (可选)惰性初始化的 init 函数,它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

本篇文章先不讲第三个函数(惰性初始化的 init 函数)的使用,有兴趣的同学可以去官方文档进行食用。

useReducer 的使用
  1. 创建变量初始仓库 initialArg 和管理者 reducer
  2. 通过 useReducer(reducer, initialArg) 来获取 state 和 dispatch

让我们通过一个简单的计数器例子来了解一下它的基础用法:

image.png

demo 解释

  1. 先创建数据仓库 initialArg,并设置一个管理者 reducerExample
  2. 通过 useReducer 来新建该 demo 的 state 和 dispatch
  3. 给点击事件套上 dispatch 事件,并传递对应参数
  4. 管理者 reducerExample 接收到对应参数后,通过 switch 执行特定行为,比如说对 state 做出改变

通过 dispatch 去派发 action,比如说上图的 type,payload

什么时候用 useReducer,什么时候用 useState?

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。

深更新的组件做性能优化

在 useReducer 的官方文档中存在这样一句介绍:

使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。

在某些场景下我们通常会将函数作为 props 传递到 child component 中去,这样的话,每次父组件 re-render 时即使我们并没有修改当作 props 的函数,子组件也会重新渲染。例子如下:

父组件

image.png

子组件

image.png

演示动画

useReducer.gif

每次点击父组件的 button 时,子组件中的 effect 中被执行了。

此时其实我们传入子组件的 callback 并没有做什么改变,我们自然期望子组件中的 Effect 不会执行。

产生这个原因的机制是 React 每次渲染都会重新执行组件函数,当重新执行父组件时会重新生成一个 callback 函数。因为 React 内部使用 Object.is 判断,所以 React 会认为子组件的 props 发生了变化。

而在 useReduce 中返回的 dispatch 正是一个函数,但是 useReducer 的好处之一便是, dispatch 不会随着 re-render 而重新分配记忆位置,比方上述我们将 dispatch 作为 props 传入 child component 中时子组件中的 Effect 也并不会被执行。

至于解决这个子组件函数重新渲染问题,除了 dispatch 还有另一种方法,可以查看下方的 useCallback 板块



6. useCallback

接下来我们来聊一聊 useCallback ,它的最大作用体现在 React 中的性能优化。

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

useCallback 接受两个参数:

  • 第一个参数是一个函数,这个函数仅会在对应依赖项发生变化之后才会被重新生成,或者说这个函数被产生「记忆」。
  • 第二个参数是一个数组,它表示第一个参数所依赖的依赖项,仅在该数组中某一项发生变化时第一个参数的函数才会「清除记忆」重新生成。

也许大多数接触 React 的朋友会好奇这个 Hook 的使用场景,此时让我们来回忆一下上面在 useReducer 板块的例子。

我们在父组件中传递了一个 callback 函数作为 props 传递给了子组件,每次渲染中我们并没有改变 callback 但是每次父组件 re-render ,React 仍然会认为 callback 发生变化从而造成多余的子组件 re-render 。

此时,使用 useCallback 就可以很好的解决这个例子,如下:

对上面 useReducer 的父组件进行更改,包裹上 useCallback

image.png

可以看到我们使用 useCallback 包裹了传入子组件的回调函数,同时第二个依赖项参数传递一个空数组。

更改后演示动画

useCallback.gif

此时即使我们多次点击按钮,子组件的 Effect 也并不会执行了。



7. useMemo

useMemo( () => fn, deps)

useMemo 同样是作为性能优化提供的 Hook ,它相比 useCallback 来说支持任意类型的值都可以被记忆。而对于这个值,我们更常用的其实是一个个函数组件,不想某个函数组件一直渲染 (例如该函数组件涉及很多的 DOM 操作)导致花费额外的性能开销时就可以考虑使用 useMemo,例如后面示例的 renderExample 组件。

如果说 useCallback 是 React 团队提供给开发者作为对于 函数 的优化手段,那么 useMemo 就可以看作用于「记忆」 从而带来性能优化。

同样它支持两个参数:

  • 第一个参数接受传入一个函数,传入的函数调用返回值会被「记忆」。仅仅当依赖项发生变化时,传入的函数才会重新执行计算新的返回结果。
  • 第二个参数同样也是一个数组,它表示第一个参数对应的依赖项。

useMemo( () => fn, deps) 相当于 useCallback(fn, deps)

useMemo 跟 useCallback 的差别

  1. useMemo 返回的是一个值,不仅仅限于函数
  2. useMemo 缓存的是一个值,useCallback 缓存的是一个函数

惯例,下面来看这个例子

image.png

当我们每次点击 button 组件 re-render 时,renderSubject 的值都会重新计算也就是说每次都会打印出 重新渲染啦!useMemo1.gif

此时让我们再换成 useMemo 包裹 renderExample ,告诉 React 「记忆」 renderExample 的值再重新试一试

image.png

useMemo2.gif

此时当我们点击页面上的 button 时,count 发生变化页面 re-render 时,因为我们使用 useMemo 传入的函数中返回 data.map((item) => <li key={item.id}>{item.name}</li>) 并且第二个参数是一个空数组。

无论页面如何 re-render ,只要依赖项不发生变化那么 useMemo 中返回的值就不会重新计算。

此时我们再再将依赖项由空数组,变为 count

image.png

useMemo3.gif

不出意外,随着依赖项 count 的改变,renderExample 这个值也重新渲染了

关于性能优化

关于 useCallback 以及 useMemo 这两个 Hook 都是 React 提供给开发者作为性能优化手段的方法。

但是大多数时候,你不需要考虑去优化不必要的重新渲染。React 是非常快的,我能想到你可以利用时间去做很多事情,比起做这些类似的优化要好得多。

对于 useCallback 和 useMemo 来说,我个人认为不合理的利用这两个 Hook 不仅仅会使代码更加复杂,同时有可能会通过调用内置的 Hook 防止依赖项和 memoized 的值被垃圾回收从而导致性能变差。

如果说,有些情况下比如交互特别复杂的图表、动画之类,使用这两个 Hook 可以使你获得了必要的性能收益,那么这些成本都是值得承担的,但最好使用之前先测量一下

官方文档指出,无需担心创建函数会导致性能问题。我们上述提供的例子仅仅是为了向大家展示它们的用法,实际场景下非常不建议这样使用。



8. useRef

const refContainer = useRef(initialValue);

类似于 useState,也是创建并保存一个变量

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

useRef Hook 的作用主要有两个:

  • 获取 Dom 元素,在 Function Component 中我们可以通过 useRef 来获取对应的 Dom 元素。

  • 多次渲染之间保证唯一值的纽带。

我们来详细讲讲第二点

useRef 会在所有的 render 中保持对返回值的唯一引用。因为所有对ref的赋值和取值拿到的都是最终的状态,并不会因为不同的 render 中存在不同的隔离。

  • useRef 相当于创建了一个变量并进行保存,不会因为组件重新渲染而重新初始化值(跟 useState 的不同处)

  • 重新赋值 ref.current 不会触发组件重新渲染

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用 回调 ref 来实现。

老规矩,下面来看个例子

image.png

在 useMemo 示例的基础上,新增了一个 useRef,对 ref.current 进行不断赋值,直到 count 的值等于5时,再 clearInterval 清除,通过 console.log() 查看变化过程

演示过程 useRef.gif

可以看到,符合预期,useRef 创建的变量,在不断对 ref.current 进行重新赋值时,组件并没有重新渲染,而是按照规则慢慢改变。



9. useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 这个 Hook 很多同学日常可能用的不是很多,但是在某些情况下它会帮助我们实现一些意向不到的效果。

  • ref 表示需要被赋值的 ref 对象。
  • createHandle 函数的返回值作为 ref.current 的值。
  • deps 依赖数组,依赖发生变化会重新执行 createHandle 函数。

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用

demo

image.png

本例中,渲染 <FancyInput ref={inputRef} /> 的父组件可以调用 inputRef.current.focus()



10. useDebugValue

useDebugValue(value , fn)

useDebugValue  可用于在 React 开发者工具中显示自定义 hook 的标签,它接受两个参数:

  • value 为我们要重点关注的变量,该参数表示在 DevTools 中显示的 hook 标志。
  • fn 表明如何格式化变量 value , 该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

例如,一个返回 Date 值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString 函数调用:

useDebugValue(date, date => date.toDateString());

当我们自定义一些 Hook 时,可以通过 useDebugValue 配合 React DevTools 快速定位我们自己定义的 Hook。

案例

image.png

这段代码中我通过 useDebug 定义了一个 hello React 的标示,此时我们来查看一下 React DevTools 的 Components 板块:

image.png

需要注意的是
  • useDebugValue应该在自定义hook中使用,如果直接在组件内使用是无效的。

  • 大部分情况下你不需要使用这个 Hook ,除非你在编写一些公共库的 Hook 时,显式标志该 Hook 。



11. 自定义 Hook

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。  

更浅显易懂的理解,自定义 Hook 是将我们需要的业务逻辑进行抽离整合到一起(组件抽离),类似 Vue3 的 Composition API,一个个自定义 Hook 可以理解为 setup(),我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

如何自定义 Hook
  1. 自定义一个 hook 函数
  2. 在该 Hook 函数内写你需要的业务逻辑(例如引入其他 Hook 等等)
  3. 返回值是一个数组,数组中第一个值是变量,第二个值是修改该变量的函数
  4. 将自定义 hook 函数 export(暴露)出去
  5. 在需要用到该自定义 Hook 的地方对该 Hook 进行引入

例如,上面第十点里的 useName 和下面的 useFriendStatus 是都是自定义的 Hook

image.png

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 Hook 的规则


🎁 谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。
🎁 我是 Smoothzjc,如果觉得写得可以的话,请点个赞吧❤
🎁 我也会在今后努力产出更多好文。
🎁 感兴趣的小伙伴也可以关注我的公众号:Smooth前端成长记录,公众号同步更新

写作不易,「点赞」+「收藏」+「转发」 谢谢支持❤

往期推荐

《一份不可多得的 Webpack 学习指南(9k 字带你入门 Webpack 并掌握常用的进阶配置)》

《Github + hexo 实现自己的个人博客、配置主题(超详细)》

《10分钟让你彻底理解如何配置子域名来部署多个项目》

《一文理解配置伪静态解决 部署项目刷新页面404问题

《带你3分钟掌握常见的水平垂直居中面试题》

《React实战:使用Antd+EMOJIALL 实现emoji表情符号的输入》

《【建议收藏】长达万字的git常用指令总结!!!适合小白及在工作中想要对git基本指令有所了解的人群》

《浅谈javascript的原型和原型链(新手懵懂想学会原型链?看这篇文章就足够啦!!!)》