赞
踩
ReactHooks
的发布已经有三年多了,它给函数式组件带来了生命周期,现如今, Hooks
逐渐取代 class
组件,相信各位 React
开发的小伙伴已经深有体会,然而你真的完全掌握hooks了吗?知道如何去做一个好的自定义hooks吗?
我们知道 ReactHooks
有 useState
设置变量, useEffect
副作用, useRef
来获取元素的所有属性,还有 useMemo
、 useCallback
来做性能优化,当然还有一个 自定义Hooks
,来创造出你所想要的 Hooks
接下来我们来看看以下几个问题,问问自己,是否全都知道:
Hooks的由来是什么?
useRef
的高级用法是什么?
useMemo
和 useCallback
是怎么做优化的?
一个好的自定义Hooks该如何设计?
如何做一个不需要 useState
就可以直接修改属性并刷新视图的自定义Hooks?
如何做一个可以监听任何事件的自定义Hooks?
如果你对以上问题有疑问,有好奇,那么这篇文章应该能够帮助到你~
本文将会以介绍自定义Hooks来解答上述问题,并结合 TS,ahooks中的钩子,以案列的形式去演示,本文过长,建议:点赞 + 收藏 哦~
注:这里讲解的自定义钩子可能会和 ahooks
上的略有不同,不会考虑过多的情况,如果用于项目,建议直接使用 ahooks
上的钩子~
如果有小伙伴不懂TS,可以看看我的这篇文章:一篇让你完全够用TS的指南
先附上一张今天的知识图,还请各位小伙伴多多支持:
react-hooks
是 React16.8
以后新增的钩子API,目的是增加代码的可复用性、逻辑性,最主要的是解决了函数式组件无状态的问题,这样既保留了函数式的简单,又解决了没有数据管理状态的缺陷
那么什么是自定义hooks呢?
自定义hooks
是在 react-hooks
基础上的一个扩展,可以根据业务、需求去制定相应的 hooks
,将常用的逻辑进行封装,从而具备复用性
hooks
本质上是一个函数,而这个函数主要就是逻辑复用,我们首先要知道一件事, hooks
的驱动条件是什么?
其实就是 props
的修改, useState
、 useReducer
的使用是无状态组件更新的条件,从而驱动自定义hooks
自定义hooks的名称是以use开头,我们设计为:
const [ xxx, ...] = useXXX(参数一,参数二...)
我们先写一个简单的小例子来了解下 自定义hooks
- // usePow.ts
- const Index = (list: number[]) => {
- return list.map((item:number) => {
- console.log(1)
- return Math.pow(item, 2)
- })
- }
- export default Index;
- // index.tsx
- import { Button } from 'antd-mobile';
- import React,{ useState } from 'react';
- import { usePow } from '@/components';
- const Index:React.FC<any> = (props)=> {
- const [flag, setFlag] = useState<boolean>(true)
- const data = usePow([1, 2, 3])
- return (
- <div>
- <div>数字:{JSON.stringify(data)}</div>
- <Button color='primary' onClick={() => {setFlag(v => !v)}}>切换</Button>
- <div>切换状态:{JSON.stringify(flag)}</div>
- </div>
- );
- }
- export default Index;
我们简单的写了个 usePow
,我们通过 usePow
给所传入的数字平方, 用切换状态的按钮表示函数内部的状态,我们来看看此时的效果:
我们发现了一个问题,为什么点击切换按钮也会触发 console.log(1)
呢?
这样明显增加了性能开销,我们的理想状态肯定不希望做无关的渲染,所以我们做自定义 hooks
的时候一定要注意,需要减少性能开销,我们为组件加入 useMemo
试试:
- import { useMemo } from 'react';
- const Index = (list: number[]) => {
- return useMemo(() => list.map((item:number) => {
- console.log(1)
- return Math.pow(item, 2)
- }), [])
- }
- export default Index;
发现此时就已经解决了这个问题,所以要非常注意一点,一个好用的自定义 hooks
,一定要配合 useMemo
、 useCallback
等 Api 一起使用。
在上述中我们讲了用 useMemo
来处理无关的渲染,接下来我们一起来看看 ReactHooks
的这些钩子的妙用(这里建议先熟知、并使用对应的 ReactHooks
,才能造出好的钩子)
当一个父组件中调用了一个子组件的时候,父组件的 state 发生变化,会导致父组件更新,而子组件虽然没有发生改变,但也会进行更新。
简单的理解下,当一个页面内容非常复杂,模块非常多的时候,函数式组件会从头更新到尾,只要一处改变,所有的模块都会进行刷新,这种情况显然是没有必要的。
我们理想的状态是各个模块只进行自己的更新,不要相互去影响,那么此时用 useMemo
是最佳的解决方案。
这里要尤其注意一点,只要父组件的状态更新,无论有没有对自组件进行操作,子组件都会进行更新, useMemo
就是为了防止这点而出现的
在讲 useMemo
之前,我们先说说 memo
, memo
的作用是结合了pureComponent纯组件和 componentShouldUpdate功能,会对传入的props进行一次对比,然后根据第二个函数返回值来进一步判断哪些props需要更新。(具体使用会在下文讲到~)
useMemo
与 memo
的理念上差不多,都是判断是否满足当前的限定条件来决定是否执行 callback
函数,而 useMemo
的第二个参数是一个数组,通过这个数组来判定是否更新回掉函数
这种方式可以运用在元素、组件、上下文中,尤其是利用在数组上,先看一个例子:
- useMemo(() => (
- <div>
- {
- list.map((item, index) => (
- <p key={index}>
- {item.name}
- </>
- )}
- }
- </div>
- ),[list])
从上面我们看出 useMemo
只有在 list
发生变化的时候才会进行渲染,从而减少了不必要的开销
总结一下 useMemo
的好处:
可以减少不必要的循环和不必要的渲染
可以减少子组件的渲染次数
通过特地的依赖进行更新,可以避免很多不必要的开销,但要注意,有时候在配合 useState
拿不到最新的值,这种情况可以考虑使用 useRef
解决
useCallback
与 useMemo
极其类似,可以说是一模一样,唯一不同的是 useMemo
返回的是函数运行的结果,而 useCallback
返回的是函数
注意:这个函数是父组件传递子组件的一个函数,防止做无关的刷新,其次,这个组件必须配合 memo
,否则不但不会提升性能,还有可能降低性能
- import React, { useState, useCallback } from 'react';
- import { Button } from 'antd-mobile';
- const MockMemo: React.FC<any> = () => {
- const [count,setCount] = useState(0)
- const [show,setShow] = useState(true)
- const add = useCallback(()=>{
- setCount(count + 1)
- },[count])
- return (
- <div>
- <div style={{display: 'flex', justifyContent: 'flex-start'}}>
- <TestButton title="普通点击" onClick={() => setCount(count + 1) }/>
- <TestButton title="useCallback点击" onClick={add}/>
- </div>
- <div style={{marginTop: 20}}>count: {count}</div>
- <Button onClick={() => {setShow(!show)}}> 切换</Button>
- </div>
- )
- }
- const TestButton = React.memo((props:any)=>{
- console.log(props.title)
- return <Button color='primary' onClick={props.onClick} style={props.title === 'useCallback点击' ? {
- marginLeft: 20
- } : undefined}>{props.title}</Button>
- })
- export default MockMemo;
我们可以看到,当点击切换按钮的时候,没有经过 useCallback
封装的函数会再次刷新,而经过 useCallback
包裹的函数不会被再次刷新
useRef 可以获取当前元素的所有属性,并且返回一个可变的ref对象,并且这个对象只有current属性,可设置initialValue
我们先看个案例:
- import React, { useState, useRef } from 'react';
- const Index:React.FC<any> = () => {
- const scrollRef = useRef<any>(null);
- const [clientHeight, setClientHeight ] = useState<number>(0)
- const [scrollTop, setScrollTop ] = useState<number>(0)
- const [scrollHeight, setScrollHeight ] = useState<number>(0)
- const onScroll = () => {
- if(scrollRef?.current){
- let clientHeight = scrollRef?.current.clientHeight; //可视区域高度
- let scrollTop = scrollRef?.current.scrollTop; //滚动条滚动高度
- let scrollHeight = scrollRef?.current.scrollHeight; //滚动内容高度
- setClientHeight(clientHeight)
- setScrollTop(scrollTop)
- setScrollHeight(scrollHeight)
- }
- }
- return (
- <div >
- <div >
- <p>可视区域高度:{clientHeight}</p>
- <p>滚动条滚动高度:{scrollTop}</p>
- <p>滚动内容高度:{scrollHeight}</p>
- </div>
- <div style={{height: 200, overflowY: 'auto'}} ref={scrollRef} onScroll={onScroll} >
- <div style={{height: 2000}}></div>
- </div>
- </div>
- );
- };
- export default Index;
从上述可知,我们可以通过 useRef
来获取对应元素的相关属性,以此来做一些操作
效果:
除了获取对应的属性值外, useRef
还有一点比较重要的特性,那就是 缓存数据
上述讲到我们封装一个合格的 自定义hooks
的时候需要结合useMemo、useCallback等Api,但我们控制变量的值用useState 有可能会导致拿到的是旧值,并且如果他们更新会带来整个组件重新执行,这种情况下,我们使用useRef将会是一个非常不错的选择
在 react-redux
的源码中,在hooks推出后, react-redux
用大量的useMemo重做了Provide等核心模块,其中就是运用useRef来缓存数据,并且所运用的 useRef() 没有一个是绑定在dom元素上的,都是做数据缓存用的
可以简单的来看一下:
- // 缓存数据
- /* react-redux 用userRef 来缓存 merge之后的 props */
- const lastChildProps = useRef()
- // lastWrapperProps 用 useRef 来存放组件真正的 props信息
- const lastWrapperProps = useRef(wrapperProps)
- //是否储存props是否处于正在更新状态
- const renderIsScheduled = useRef(false)
- //更新数据
- function captureWrapperProps(
- lastWrapperProps,
- lastChildProps,
- renderIsScheduled,
- wrapperProps,
- actualChildProps,
- childPropsFromStoreUpdate,
- notifyNestedSubs
- ) {
- lastWrapperProps.current = wrapperProps
- lastChildProps.current = actualChildProps
- renderIsScheduled.current = false
- }
- ```
- 我们看到 `react-redux` 用重新赋值的方法,改变了缓存的数据源,减少了不必要的更新,如过采取`useState`势必会重新渲染
- ### useLatest
- 经过上面的讲解我们知道`useRef` 可以拿到最新值,我们可以进行简单的封装,这样做的好处是:**可以随时确保获取的是最新值,并且也可以解决闭包问题**
- ```ts
- import { useRef } from 'react';
- const useLatest = <T>(value: T) => {
- const ref = useRef(value)
- ref.current = value
- return ref
- };
- export default useLatest;
- ```
- ### 结合useMemo和useRef封装useCreation
- **useCreation** :是 `useMemo` 或 `useRef`的替代品。换言之,`useCreation`这个钩子增强了 `useMemo` 和 `useRef`,让这个钩子可以替换这两个钩子。(来自[ahooks-useCreation](https://ahooks.js.org/zh-CN/hooks/use-creation))
- - `useMemo`的值不一定是最新的值,但`useCreation`可以保证拿到的值一定是最新的值
- - 对于复杂常量的创建,`useRef`容易出现潜在的的性能隐患,但`useCreation`可以避免
- 这里的性能隐患是指:
ts // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了 const a = useRef(new Subject())
// 通过 factory 函数,可以避免性能隐患 const b = useCreation(() => new Subject(), [])
- 接下来我们来看看如何封装一个`useCreation`,首先我们要明白以下三点:
- - 第一点:先确定参数,`useCreation` 的参数与`useMemo`的一致,第一个参数是函数,第二个参数参数是可变的数组
- - 第二点:我们的值要保存在 `useRef`中,这样可以将值缓存,从而减少无关的刷新
- - 第三点:更新值的判断,怎么通过第二个参数来判断是否更新 `useRef`里的值。
- 明白了一上三点我们就可以自己实现一个`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;
- 在`useRef`判断是否更新值通过`initialized` 和 `depsAreSame`来判断,其中`depsAreSame`通过存储在 `useRef`下的`deps`(旧值) 和 新传入的 `deps`(新值)来做对比,判断两数组的数据是否一致,来确定是否更新
- ### 验证 useCreation
- 接下来我们写个小例子,来验证下 `useCreation`是否能满足我们的要求:
tsx import React, { useState } from 'react'; import { Button } from 'antd-mobile'; import { useCreation } from '@/components';
- const Index: React.FC<any> = () => {
- const [_, setFlag] = useState<boolean>(false)
- const getNowData = () => {
- return Math.random()
- }
- const nowData = useCreation(() => getNowData(), []);
- return (
- <div style={{padding: 50}}>
- <div>正常的函数: {getNowData()}</div>
- <div>useCreation包裹后的: {nowData}</div>
- <Button color='primary' onClick={() => {setFlag(v => !v)}}> 渲染</Button>
- </div>
- )
- }
- export default Index;
- ![useCreation.gif](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa5645ef84d141878142032ae2079bdc~tplv-k3u1fbpfcp-watermark.image?)
- 我们可以看到,当我们做无关的`state`改变的时候,正常的函数也会刷新,但`useCreation`没有刷新,从而增强了渲染的性能~
- ## useEffect
- `useEffect`相信各位小伙伴已经用的熟的不能再熟了,我们可以使用`useEffect`来模拟下`class`的`componentDidMount`和`componentWillUnmount`的功能。
- ### useMount
- 这个钩子不必多说,只是简化了使用`useEffect`的第二个参数:
ts import { useEffect } from 'react';
- const useMount = (fn: () => void) => {
- useEffect(() => {
- fn?.();
- }, []);
- };
- export default useMount;
- ### useUnmount
- 这个需要注意一个点,就是使用`useRef`来确保所传入的函数为最新的状态,所以可以结合上述讲的**useLatest**结合使用
ts import { useEffect, useRef } from 'react';
- const useUnmount = (fn: () => void) => {
- const ref = useRef(fn);
- ref.current = fn;
- useEffect(
- () => () => {
- fn?.()
- },
- [],
- );
- };
- export default useUnmount;
### 结合`useMount`和`useUnmount`做个小例子
ts import { Button, Toast } from 'antd-mobile'; import React,{ useState } from 'react'; import { useMount, useUnmount } from '@/components';
- const Child = () => {
- useMount(() => {
- Toast.show('首次渲染')
- });
- useUnmount(() => {
- Toast.show('组件已卸载')
- })
- return <div>你好,我是小杜杜</div>
- }
- const Index:React.FC<any> = (props)=> {
- const [flag, setFlag] = useState<boolean>(false)
- return (
- <div style={{padding: 50}}>
- <Button color='primary' onClick={() => {setFlag(v => !v)}}>切换 {flag ? 'unmount' : 'mount'}</Button>
- {flag && <Child />}
- </div>
- );
- }
- export default Index;
- 效果如下:
- ![img5.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27b1cfa623a944eb9056b62eeafaba5f~tplv-k3u1fbpfcp-watermark.image?)
- ### useUpdate
- **useUpdate**:强制更新
- 有的时候我们需要组件强制更新,这个时候就可以使用这个钩子:
ts import { useCallback, useState } from 'react';
- const useUpdate = () => {
- const [, setState] = useState({});
- return useCallback(() => setState({}), []);
- };
- export default useUpdate;
- //示例:
- import { Button } from 'antd-mobile';
- import React from 'react';
- import { useUpdate } from '@/components';
- const Index:React.FC<any> = (props)=> {
- const update = useUpdate();
- return (
- <div style={{padding: 50}}>
- <div>时间:{Date.now()}</div>
- <Button color='primary' onClick={update}>更新时间</Button>
- </div>
- );
- }
- export default Index;
- 效果如下:
- ![img6.gif](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bbe4ebe0e17f439693b48eac899e3f67~tplv-k3u1fbpfcp-watermark.image?)
- # 案例
- ## 案例1: useReactive
- **useReactive**: 一种具备**响应式**的`useState`
- 缘由:我们知道用`useState`可以定义变量其格式为:
- `const [count, setCount] = useState<number>(0)`
- 通过`setCount`来设置,`count`来获取,使用这种方式才能够渲染视图
- 来看看正常的操作,像这样 `let count = 0; count =7` 此时`count`的值就是7,也就是说数据是响应式的
- 那么我们可不可以将 `useState`也写成**响应式**的呢?我可以自由设置**count的值,并且可以随时获取到count的最新值**,而不是通过`setCount`来设置。
- 我们来想想怎么去实现一个具备 **响应式** 特点的 `useState` 也就是 `useRective`,提出以下疑问,感兴趣的,可以先自行思考一下:
- - 这个钩子的出入参该怎么设定?
- - 如何将数据制作成响应式(毕竟普通的操作无法刷新视图)?
- - 如何使用`TS`去写,完善其类型?
- - 如何更好的去优化?
- ### 分析
- 以上四个小问题,最关键的就是`第二个`,我们如何将数据弄成**响应式**,想要弄成响应式,就必须监听到值的变化,在做出更改,也就是说,我们对这个数进行操作的时候,要进行相应的**拦截**,这时就需要`ES6`的一个知识点:**Proxy**
- 在这里会用到 **Proxy**和**Reflect**的点,感兴趣的可以看看我的这篇文章:[ 本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/411820推荐阅读
相关标签
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。