当前位置:   article > 正文

【React】1485- 搞懂这12个Hooks,保证让你玩转React

react formattedres

ReactHooks的发布已经有三年多了,它给函数式组件带来了生命周期,现如今, Hooks逐渐取代 class组件,相信各位 React 开发的小伙伴已经深有体会,然而你真的完全掌握hooks了吗?知道如何去做一个好的自定义hooks吗?

我们知道 ReactHooksuseState设置变量, useEffect副作用, useRef来获取元素的所有属性,还有 useMemouseCallback来做性能优化,当然还有一个 自定义Hooks,来创造出你所想要的 Hooks

接下来我们来看看以下几个问题,问问自己,是否全都知道:

  • Hooks的由来是什么?

  • useRef的高级用法是什么?

  • useMemo 和 useCallback 是怎么做优化的?

  • 一个好的自定义Hooks该如何设计?

  • 如何做一个不需要 useState就可以直接修改属性并刷新视图的自定义Hooks?

  • 如何做一个可以监听任何事件的自定义Hooks?

如果你对以上问题有疑问,有好奇,那么这篇文章应该能够帮助到你~

本文将会以介绍自定义Hooks来解答上述问题,并结合 TSahooks中的钩子,以案列的形式去演示,本文过长,建议:点赞 + 收藏 哦~

注:这里讲解的自定义钩子可能会和 ahooks上的略有不同,不会考虑过多的情况,如果用于项目,建议直接使用 ahooks上的钩子~

如果有小伙伴不懂TS,可以看看我的这篇文章:一篇让你完全够用TS的指南

先附上一张今天的知识图,还请各位小伙伴多多支持:

d24b6ba14a1aa613cf13d4daec7a7fee.png

自定义Hooks是什么?

react-hooksReact16.8以后新增的钩子API,目的是增加代码的可复用性、逻辑性,最主要的是解决了函数式组件无状态的问题,这样既保留了函数式的简单,又解决了没有数据管理状态的缺陷

那么什么是自定义hooks呢?

自定义hooks是在 react-hooks基础上的一个扩展,可以根据业务、需求去制定相应的 hooks,将常用的逻辑进行封装,从而具备复用性

如何设计一个自定义Hooks

hooks本质上是一个函数,而这个函数主要就是逻辑复用,我们首先要知道一件事, hooks的驱动条件是什么?

其实就是 props的修改, useStateuseReducer的使用是无状态组件更新的条件,从而驱动自定义hooks

通用模式

自定义hooks的名称是以use开头,我们设计为:

const [ xxx, ...] = useXXX(参数一,参数二...)

简单的小例子:usePow

我们先写一个简单的小例子来了解下 自定义hooks

  1. // usePow.ts
  2. const Index = (list: number[]) => {
  3. return list.map((item:number) => {
  4. console.log(1)
  5. return Math.pow(item, 2)
  6. })
  7. }
  8. export default Index;
  9. // index.tsx
  10. import { Button } from 'antd-mobile';
  11. import React,{ useState } from 'react';
  12. import { usePow } from '@/components';
  13. const Index:React.FC<any> = (props)=> {
  14. const [flag, setFlag] = useState<boolean>(true)
  15. const data = usePow([1, 2, 3])
  16. return (
  17. <div>
  18. <div>数字:{JSON.stringify(data)}</div>
  19. <Button color='primary' onClick={() => {setFlag(v => !v)}}>切换</Button>
  20. <div>切换状态:{JSON.stringify(flag)}</div>
  21. </div>
  22. );
  23. }
  24. export default Index;

我们简单的写了个 usePow,我们通过 usePow 给所传入的数字平方, 用切换状态的按钮表示函数内部的状态,我们来看看此时的效果:

6f1fdf601e469e1a63a9bd45391df1b1.gif

我们发现了一个问题,为什么点击切换按钮也会触发 console.log(1)呢?

这样明显增加了性能开销,我们的理想状态肯定不希望做无关的渲染,所以我们做自定义 hooks的时候一定要注意,需要减少性能开销,我们为组件加入 useMemo试试:

  1. import { useMemo } from 'react';
  2. const Index = (list: number[]) => {
  3. return useMemo(() => list.map((item:number) => {
  4. console.log(1)
  5. return Math.pow(item, 2)
  6. }), [])
  7. }
  8. export default Index;

2cd367d5422b8959f4ec45bd2f003a0e.gif

发现此时就已经解决了这个问题,所以要非常注意一点,一个好用的自定义 hooks,一定要配合 useMemouseCallback等 Api 一起使用。

玩转React Hooks

在上述中我们讲了用 useMemo来处理无关的渲染,接下来我们一起来看看 ReactHooks的这些钩子的妙用(这里建议先熟知、并使用对应的 ReactHooks,才能造出好的钩子)

useMemo

当一个父组件中调用了一个子组件的时候,父组件的 state 发生变化,会导致父组件更新,而子组件虽然没有发生改变,但也会进行更新。

简单的理解下,当一个页面内容非常复杂,模块非常多的时候,函数式组件会从头更新到尾,只要一处改变,所有的模块都会进行刷新,这种情况显然是没有必要的。

我们理想的状态是各个模块只进行自己的更新,不要相互去影响,那么此时用 useMemo是最佳的解决方案。

这里要尤其注意一点,只要父组件的状态更新,无论有没有对自组件进行操作,子组件都会进行更新useMemo就是为了防止这点而出现的

在讲 useMemo 之前,我们先说说 memo, memo的作用是结合了pureComponent纯组件和 componentShouldUpdate功能,会对传入的props进行一次对比,然后根据第二个函数返回值来进一步判断哪些props需要更新。(具体使用会在下文讲到~)

useMemomemo的理念上差不多,都是判断是否满足当前的限定条件来决定是否执行 callback函数,而 useMemo的第二个参数是一个数组,通过这个数组来判定是否更新回掉函数

这种方式可以运用在元素、组件、上下文中,尤其是利用在数组上,先看一个例子:

  1. useMemo(() => (
  2. <div>
  3. {
  4. list.map((item, index) => (
  5. <p key={index}>
  6. {item.name}
  7. </>
  8. )}
  9. }
  10. </div>
  11. ),[list])

从上面我们看出 useMemo只有在 list发生变化的时候才会进行渲染,从而减少了不必要的开销

总结一下 useMemo的好处:

  • 可以减少不必要的循环和不必要的渲染

  • 可以减少子组件的渲染次数

  • 通过特地的依赖进行更新,可以避免很多不必要的开销,但要注意,有时候在配合 useState拿不到最新的值,这种情况可以考虑使用 useRef解决

useCallback

useCallbackuseMemo极其类似,可以说是一模一样,唯一不同的是 useMemo返回的是函数运行的结果,而 useCallback返回的是函数

注意:这个函数是父组件传递子组件的一个函数,防止做无关的刷新,其次,这个组件必须配合 memo,否则不但不会提升性能,还有可能降低性能

  1. import React, { useState, useCallback } from 'react';
  2. import { Button } from 'antd-mobile';
  3. const MockMemo: React.FC<any> = () => {
  4. const [count,setCount] = useState(0)
  5. const [show,setShow] = useState(true)
  6. const add = useCallback(()=>{
  7. setCount(count + 1)
  8. },[count])
  9. return (
  10. <div>
  11. <div style={{display: 'flex', justifyContent: 'flex-start'}}>
  12. <TestButton title="普通点击" onClick={() => setCount(count + 1) }/>
  13. <TestButton title="useCallback点击" onClick={add}/>
  14. </div>
  15. <div style={{marginTop: 20}}>count: {count}</div>
  16. <Button onClick={() => {setShow(!show)}}> 切换</Button>
  17. </div>
  18. )
  19. }
  20. const TestButton = React.memo((props:any)=>{
  21. console.log(props.title)
  22. return <Button color='primary' onClick={props.onClick} style={props.title === 'useCallback点击' ? {
  23. marginLeft: 20
  24. } : undefined}>{props.title}</Button>
  25. })
  26. export default MockMemo;

2ef92b083263fdace0eb8d37bd1ff162.gif

我们可以看到,当点击切换按钮的时候,没有经过 useCallback封装的函数会再次刷新,而经过 useCallback包裹的函数不会被再次刷新

useRef

useRef 可以获取当前元素的所有属性,并且返回一个可变的ref对象,并且这个对象只有current属性,可设置initialValue

通过useRef获取对应的属性值

我们先看个案例:

  1. import React, { useState, useRef } from 'react';
  2. const Index:React.FC<any> = () => {
  3. const scrollRef = useRef<any>(null);
  4. const [clientHeight, setClientHeight ] = useState<number>(0)
  5. const [scrollTop, setScrollTop ] = useState<number>(0)
  6. const [scrollHeight, setScrollHeight ] = useState<number>(0)
  7. const onScroll = () => {
  8. if(scrollRef?.current){
  9. let clientHeight = scrollRef?.current.clientHeight; //可视区域高度
  10. let scrollTop = scrollRef?.current.scrollTop; //滚动条滚动高度
  11. let scrollHeight = scrollRef?.current.scrollHeight; //滚动内容高度
  12. setClientHeight(clientHeight)
  13. setScrollTop(scrollTop)
  14. setScrollHeight(scrollHeight)
  15. }
  16. }
  17. return (
  18. <div >
  19. <div >
  20. <p>可视区域高度:{clientHeight}</p>
  21. <p>滚动条滚动高度:{scrollTop}</p>
  22. <p>滚动内容高度:{scrollHeight}</p>
  23. </div>
  24. <div style={{height: 200, overflowY: 'auto'}} ref={scrollRef} onScroll={onScroll} >
  25. <div style={{height: 2000}}></div>
  26. </div>
  27. </div>
  28. );
  29. };
  30. export default Index;

从上述可知,我们可以通过 useRef来获取对应元素的相关属性,以此来做一些操作

效果:b25f6f2bf0d4787b71ec6f3e8a54de74.gif

缓存数据

除了获取对应的属性值外, useRef还有一点比较重要的特性,那就是 缓存数据

上述讲到我们封装一个合格的 自定义hooks的时候需要结合useMemouseCallback等Api,但我们控制变量的值用useState 有可能会导致拿到的是旧值,并且如果他们更新会带来整个组件重新执行,这种情况下,我们使用useRef将会是一个非常不错的选择

react-redux的源码中,在hooks推出后, react-redux用大量的useMemo重做了Provide等核心模块,其中就是运用useRef来缓存数据,并且所运用的 useRef() 没有一个是绑定在dom元素上的,都是做数据缓存用的

可以简单的来看一下:

  1. // 缓存数据
  2. /* react-redux 用userRef 来缓存 merge之后的 props */
  3. const lastChildProps = useRef()
  4. // lastWrapperProps 用 useRef 来存放组件真正的 props信息
  5. const lastWrapperProps = useRef(wrapperProps)
  6. //是否储存props是否处于正在更新状态
  7. const renderIsScheduled = useRef(false)
  8. //更新数据
  9. function captureWrapperProps(
  10. lastWrapperProps,
  11. lastChildProps,
  12. renderIsScheduled,
  13. wrapperProps,
  14. actualChildProps,
  15. childPropsFromStoreUpdate,
  16. notifyNestedSubs
  17. ) {
  18. lastWrapperProps.current = wrapperProps
  19. lastChildProps.current = actualChildProps
  20. renderIsScheduled.current = false
  21. }
  22. ```
  23. 我们看到 `react-redux` 用重新赋值的方法,改变了缓存的数据源,减少了不必要的更新,如过采取`useState`势必会重新渲染
  24. ### useLatest
  25. 经过上面的讲解我们知道`useRef` 可以拿到最新值,我们可以进行简单的封装,这样做的好处是:**可以随时确保获取的是最新值,并且也可以解决闭包问题**
  26. ```ts
  27. import { useRef } from 'react';
  28. const useLatest = <T>(value: T) => {
  29. const ref = useRef(value)
  30. ref.current = value
  31. return ref
  32. };
  33. export default useLatest;
  34. ```
  35. ### 结合useMemo和useRef封装useCreation
  36. **useCreation** :是 `useMemo` 或 `useRef`的替代品。换言之,`useCreation`这个钩子增强了 `useMemo` 和 `useRef`,让这个钩子可以替换这两个钩子。(来自[ahooks-useCreation](https://ahooks.js.org/zh-CN/hooks/use-creation))
  37. - `useMemo`的值不一定是最新的值,但`useCreation`可以保证拿到的值一定是最新的值
  38. - 对于复杂常量的创建,`useRef`容易出现潜在的的性能隐患,但`useCreation`可以避免
  39. 这里的性能隐患是指:

ts // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了 const a = useRef(new Subject())

// 通过 factory 函数,可以避免性能隐患 const b = useCreation(() => new Subject(), [])

  1. 接下来我们来看看如何封装一个`useCreation`,首先我们要明白以下三点:
  2. - 第一点:先确定参数,`useCreation` 的参数与`useMemo`的一致,第一个参数是函数,第二个参数参数是可变的数组
  3. - 第二点:我们的值要保存在 `useRef`中,这样可以将值缓存,从而减少无关的刷新
  4. - 第三点:更新值的判断,怎么通过第二个参数来判断是否更新 `useRef`里的值。
  5. 明白了一上三点我们就可以自己实现一个`useCreation`

ts import { useRef } from 'react'; import type { DependencyList } from 'react';

const depsAreSame = (oldDeps: DependencyList, deps: DependencyList):boolean => { if(oldDeps === deps) return true

for(let i = 0; i < oldDeps.length; i++) { // 判断两个值是否是同一个值 if(!Object.is(oldDeps[i], deps[i])) return false }

return true }

const useCreation =(fn:() => T, deps: DependencyList)=> {

const { current } = useRef({ deps, obj: undefined as undefined | T , initialized: false })

if(current.initialized === false || !depsAreSame(current.deps, deps)) { current.deps = deps; current.obj = fn(); current.initialized = true; }

return current.obj as T }

export default useCreation;

  1. `useRef`判断是否更新值通过`initialized``depsAreSame`来判断,其中`depsAreSame`通过存储在 `useRef`下的`deps`(旧值) 和 新传入的 `deps`(新值)来做对比,判断两数组的数据是否一致,来确定是否更新
  2. ### 验证 useCreation
  3. 接下来我们写个小例子,来验证下 `useCreation`是否能满足我们的要求:

tsx import React, { useState } from 'react'; import { Button } from 'antd-mobile'; import { useCreation } from '@/components';

  1. const Index: React.FC<any> = () => {
  2. const [_, setFlag] = useState<boolean>(false)
  3. const getNowData = () => {
  4. return Math.random()
  5. }
  6. const nowData = useCreation(() => getNowData(), []);
  7. return (
  8. <div style={{padding: 50}}>
  9. <div>正常的函数: {getNowData()}</div>
  10. <div>useCreation包裹后的: {nowData}</div>
  11. <Button color='primary' onClick={() => {setFlag(v => !v)}}> 渲染</Button>
  12. </div>
  13. )
  14. }
  15. export default Index;
  1. ![useCreation.gif](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa5645ef84d141878142032ae2079bdc~tplv-k3u1fbpfcp-watermark.image?)
  2. 我们可以看到,当我们做无关的`state`改变的时候,正常的函数也会刷新,但`useCreation`没有刷新,从而增强了渲染的性能~
  3. ## useEffect
  4. `useEffect`相信各位小伙伴已经用的熟的不能再熟了,我们可以使用`useEffect`来模拟下`class``componentDidMount``componentWillUnmount`的功能。
  5. ### useMount
  6. 这个钩子不必多说,只是简化了使用`useEffect`的第二个参数:

ts import { useEffect } from 'react';

  1. const useMount = (fn: () => void) => {
  2. useEffect(() => {
  3. fn?.();
  4. }, []);
  5. };
  6. export default useMount;
  1. ### useUnmount
  2. 这个需要注意一个点,就是使用`useRef`来确保所传入的函数为最新的状态,所以可以结合上述讲的**useLatest**结合使用

ts import { useEffect, useRef } from 'react';

  1. const useUnmount = (fn: () => void) => {
  2. const ref = useRef(fn);
  3. ref.current = fn;
  4. useEffect(
  5. () => () => {
  6. fn?.()
  7. },
  8. [],
  9. );
  10. };
  11. export default useUnmount;
### 结合`useMount``useUnmount`做个小例子

ts import { Button, Toast } from 'antd-mobile'; import React,{ useState } from 'react'; import { useMount, useUnmount } from '@/components';

  1. const Child = () => {
  2. useMount(() => {
  3. Toast.show('首次渲染')
  4. });
  5. useUnmount(() => {
  6. Toast.show('组件已卸载')
  7. })
  8. return <div>你好,我是小杜杜</div>
  9. }
  10. const Index:React.FC<any> = (props)=> {
  11. const [flag, setFlag] = useState<boolean>(false)
  12. return (
  13. <div style={{padding: 50}}>
  14. <Button color='primary' onClick={() => {setFlag(v => !v)}}>切换 {flag ? 'unmount' : 'mount'}</Button>
  15. {flag && <Child />}
  16. </div>
  17. );
  18. }
  19. export default Index;
  1. 效果如下:
  2. ![img5.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27b1cfa623a944eb9056b62eeafaba5f~tplv-k3u1fbpfcp-watermark.image?)
  3. ### useUpdate
  4. **useUpdate**:强制更新
  5. 有的时候我们需要组件强制更新,这个时候就可以使用这个钩子:

ts import { useCallback, useState } from 'react';

  1. const useUpdate = () => {
  2. const [, setState] = useState({});
  3. return useCallback(() => setState({}), []);
  4. };
  5. export default useUpdate;
  6. //示例:
  7. import { Button } from 'antd-mobile';
  8. import React from 'react';
  9. import { useUpdate } from '@/components';
  10. const Index:React.FC<any> = (props)=> {
  11. const update = useUpdate();
  12. return (
  13. <div style={{padding: 50}}>
  14. <div>时间:{Date.now()}</div>
  15. <Button color='primary' onClick={update}>更新时间</Button>
  16. </div>
  17. );
  18. }
  19. export default Index;
  1. 效果如下:
  2. ![img6.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bbe4ebe0e17f439693b48eac899e3f67~tplv-k3u1fbpfcp-watermark.image?)
  3. # 案例
  4. ## 案例1: useReactive
  5. **useReactive**: 一种具备**响应式**的`useState`
  6. 缘由:我们知道用`useState`可以定义变量其格式为:
  7. `const [count, setCount] = useState<number>(0)`
  8. 通过`setCount`来设置,`count`来获取,使用这种方式才能够渲染视图
  9. 来看看正常的操作,像这样 `let count = 0; count =7` 此时`count`的值就是7,也就是说数据是响应式的
  10. 那么我们可不可以将 `useState`也写成**响应式**的呢?我可以自由设置**count的值,并且可以随时获取到count的最新值**,而不是通过`setCount`来设置。
  11. 我们来想想怎么去实现一个具备 **响应式** 特点的 `useState` 也就是 `useRective`,提出以下疑问,感兴趣的,可以先自行思考一下:
  12. - 这个钩子的出入参该怎么设定?
  13. - 如何将数据制作成响应式(毕竟普通的操作无法刷新视图)?
  14. - 如何使用`TS`去写,完善其类型?
  15. - 如何更好的去优化?
  16. ### 分析
  17. 以上四个小问题,最关键的就是`第二个`,我们如何将数据弄成**响应式**,想要弄成响应式,就必须监听到值的变化,在做出更改,也就是说,我们对这个数进行操作的时候,要进行相应的**拦截**,这时就需要`ES6`的一个知识点:**Proxy**
  18. 在这里会用到 **Proxy**和**Reflect**的点,感兴趣的可以看看我的这篇文章:[ 本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/411820
    推荐阅读
    相关标签