点击上方 前端Q,关注公众号
回复加群,加入前端Q技术交流群
大家好,React Hooks
的发布已经有三年多了,它给函数式组件带来了生命周期,现如今,Hooks
逐渐取代class
组件,相信各位 React
开发的小伙伴已经深有体会,然而你真的完全掌握hooks了吗?知道如何去做一个好的自定义hooks吗?
我们知道React Hooks
有useState
设置变量,useEffect
副作用,useRef
来获取元素的所有属性,还有useMemo
、useCallback
来做性能优化,当然还有一个自定义Hooks
,来创造出你所想要的Hooks
接下来我们来看看以下几个问题,问问自己,是否全都知道:
Hooks的由来是什么?
useRef
的高级用法是什么?useMemo
和useCallback
是怎么做优化的?一个好的自定义Hooks该如何设计?
如何做一个不需要
useState
就可以直接修改属性并刷新视图的自定义Hooks?如何做一个可以监听任何事件的自定义Hooks?
如果你对以上问题有疑问,有好奇,那么这篇文章应该能够帮助到你~
本文将会以介绍自定义Hooks来解答上述问题,并结合 TS,ahooks中的钩子,以案列的形式去演示,本文过长,建议:点赞 + 收藏 哦~
注:这里讲解的自定义钩子可能会和 ahooks
上的略有不同,不会考虑过多的情况,如果用于项目,建议直接使用ahooks
上的钩子~
如果有小伙伴不懂TS,可以看看我的这篇文章:一篇让你完全够用TS的指南[1]
先附上一张今天的知识图,还请各位小伙伴多多支持:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
自定义Hooks是什么?
react-hooks
是React16.8
以后新增的钩子API,目的是增加代码的可复用性、逻辑性,最主要的是解决了函数式组件无状态的问题,这样既保留了函数式的简单,又解决了没有数据管理状态的缺陷
那么什么是自定义hooks呢?
自定义hooks
是在react-hooks
基础上的一个扩展,可以根据业务、需求去制定相应的hooks
,将常用的逻辑进行封装,从而具备复用性
如何设计一个自定义Hooks
hooks
本质上是一个函数,而这个函数主要就是逻辑复用,我们首先要知道一件事,hooks
的驱动条件是什么?
其实就是props
的修改,useState
、useReducer
的使用是无状态组件更新的条件,从而驱动自定义hooks
通用模式
自定义hooks的名称是以use开头,我们设计为:
const [ xxx, ...] = useXXX(参数一,参数二...)
简单的小例子:usePow
我们先写一个简单的小例子来了解下自定义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
给所传入的数字平方, 用切换状态的按钮表示函数内部的状态,我们来看看此时的效果:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
我们发现了一个问题,为什么点击切换按钮也会触发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;
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
发现此时就已经解决了这个问题,所以要非常注意一点,一个好用的自定义hooks
,一定要配合useMemo
、useCallback
等 Api 一起使用。
玩转React Hooks
在上述中我们讲了用 useMemo
来处理无关的渲染,接下来我们一起来看看React Hooks
的这些钩子的妙用(这里建议先熟知、并使用对应的React Hooks
,才能造出好的钩子)
useMemo
当一个父组件中调用了一个子组件的时候,父组件的 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
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;
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
我们可以看到,当点击切换按钮的时候,没有经过 useCallback
封装的函数会再次刷新,而进过过 useCallback
包裹的函数不会被再次刷新
useRef
useRef 可以获取当前元素的所有属性,并且返回一个可变的ref对象,并且这个对象只有current属性,可设置initialValue
通过useRef获取对应的属性值
我们先看个案例:
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
可以拿到最新值,我们可以进行简单的封装,这样做的好处是:可以随时确保获取的是最新值,并且也可以解决闭包问题
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[2])
useMemo
的值不一定是最新的值,但useCreation
可以保证拿到的值一定是最新的值对于复杂常量的创建,
useRef
容易出现潜在的的性能隐患,但useCreation
可以避免
这里的性能隐患是指:
// 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了
const a = useRef(new Subject())
// 通过 factory 函数,可以避免性能隐患
const b = useCreation(() => new Subject(), [])
接下来我们来看看如何封装一个useCreation
,首先我们要明白以下三点:
第一点:先确定参数,
useCreation
的参数与useMemo
的一致,第一个参数是函数,第二个参数参数是可变的数组第二点:我们的值要保存在
useRef
中,这样可以将值缓存,从而减少无关的刷新第三点:更新值的判断,怎么通过第二个参数来判断是否更新
useRef
里的值。
明白了一上三点我们就可以自己实现一个useCreation
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 = <T>(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
是否能满足我们的要求:
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;
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
我们可以看到,当我们做无关的state
改变的时候,正常的函数也会刷新,但useCreation
没有刷新,从而增强了渲染的性能~
useEffect
useEffect
相信各位小伙伴已经用的熟的不能再熟了,我们可以使用useEffect
来模拟下class
的componentDidMount
和componentWillUnmount
的功能。
useMount
这个钩子不必多说,只是简化了使用useEffect
的第二个参数:
import { useEffect } from 'react';
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
export default useMount;
useUnmount
这个需要注意一个点,就是使用useRef
来确保所传入的函数为最新的状态,所以可以结合上述讲的useLatest结合使用
import { useEffect, useRef } from 'react';
const useUnmount = (fn: () => void) => {
const ref = useRef(fn);
ref.current = fn;
useEffect(
() => () => {
fn?.()
},
[],
);
};
export default useUnmount;
结合useMount
和useUnmount
做个小例子
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;
效果如下:
useUpdate
useUpdate:强制更新
有的时候我们需要组件强制更新,这个时候就可以使用这个钩子:
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;
效果如下:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
案例
案例1: useReactive
useReactiv: 一种具备响应式的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的点,感兴趣的可以看看我的这篇文章:🔥花一个小时,迅速了解ES6\~ES12的全部特性[3]
Proxy:接受的参数是对象,所以第一个问题也解决了,入参就为对象。那么如何去刷新视图呢?这里就使用上述的useUpdate来强制刷新,使数据更改。
至于优化这一块,使用上文说的useCreation
就好,再配合useRef
来放initialState
即可
代码
import { useRef } from 'react';
import { useUpdate, useCreation } from '../index';
const observer = <T extends Record<string, any>>(initialVal: T, cb: () => void): T => {
const proxy = new Proxy<T>(initialVal, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
return typeof res === 'object' ? observer(res, cb) : Reflect.get(target, key);
},
set(target, key, val) {
const ret = Reflect.set(target, key, val);
cb();
return ret;
},
});
return proxy;
}
const useReactive = <T extends Record<string, any>>(initialState: T):T => {
const ref = useRef<T>(initialState);
const update = useUpdate();
const state = useCreation(() => {
return observer(ref.current, () => {
update();
});
}, []);
return state
};
export default useReactive;
这里先说下TS
,因为我们不知道会传递什么类型的initialState
所以在这需要使用泛型,我们接受的参数是对象,可就是 key-value 的形式,其中 key 为 string,value 可以是 任意类型,所以我们使用 Record<string, any>
有不熟悉的小伙伴可以看看我的这篇文章:一篇让你完全够用TS的指南[4](又推销一遍,有点打广告,别在意~)
再来说下拦截这块
,我们只需要拦截设置(set) 和 获取(get) 即可,其中:
设置这块,需要改变是图,也就是说需要,使用useUpdate来强制刷新
获取这块,需要判断其是否为对象,是的话继续递归,不是的话返回就行
验证
接下来我们来验证一下我们写的 useReactive
,我们将以 字符串、数字、布尔、数组、函数、计算属性几个方面去验证一下:
import { Button } from 'antd-mobile';
import React from 'react';
import { useReactive } from '@/components'
const Index:React.FC<any> = (props)=> {
const state = useReactive<any>({
count: 0,
name: '小杜杜',
flag: true,
arr: [],
bugs: ['小杜杜', 'react', 'hook'],
addBug(bug:string) {
this.bugs.push(bug);
},
get bugsCount() {
return this.bugs.length;
},
})
return (
<div style={{padding: 20}}>
<div style={{fontWeight: 'bold'}}>基本使用:</div>
<div style={{marginTop: 8}}> 对数字进行操作:{state.count}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color='primary' onClick={() => state.count++ } >加1</Button>
<Button color='primary' style={{marginLeft: 8}} onClick={() => state.count-- } >减1</Button>
<Button color='primary' style={{marginLeft: 8}} onClick={() => state.count = 7 } >设置为7</Button>
</div>
<div style={{marginTop: 8}}> 对字符串进行操作:{state.name}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color='primary' onClick={() => state.name = '小杜杜' } >设置为小杜杜</Button>
<Button color='primary' style={{marginLeft: 8}} onClick={() => state.name = 'Domesy'} >设置为Domesy</Button>
</div>
<div style={{marginTop: 8}}> 对布尔值进行操作:{JSON.stringify(state.flag)}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color='primary' onClick={() => state.flag = !state.flag } >切换状态</Button>
</div>
<div style={{marginTop: 8}}> 对数组进行操作:{JSON.stringify(state.arr)}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color="primary" onClick={() => state.arr.push(Math.floor(Math.random() * 100))} >push</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.pop()} >pop</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.shift()} >shift</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.unshift(Math.floor(Math.random() * 100))} >unshift</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.reverse()} >reverse</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.sort()} >sort</Button>
</div>
<div style={{fontWeight: 'bold', marginTop: 8}}>计算属性:</div>
<div style={{marginTop: 8}}>数量:{ state.bugsCount } 个</div>
<div style={{margin: '8px 0'}}>
<form
onSubmit={(e) => {
state.bug ? state.addBug(state.bug) : state.addBug('domesy')
state.bug = '';
e.preventDefault();
}}
>
<input type="text" value={state.bug} onChange={(e) => (state.bug = e.target.value)} />
<button type="submit" style={{marginLeft: 8}} >增加</button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.bugs.pop()}>删除</Button>
</form>
</div>
<ul>
{
state.bugs.map((bug:any, index:number) => (
<li key={index}>{bug}</li>
))
}
</ul>
</div>
);
}
export default Index;
效果如下:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
案例2: useEventListener
缘由:我们监听各种事件的时候需要做监听,如:监听点击事件、键盘事件、滚动事件等,我们将其统一封装起来,方便后续调用
说白了就是在addEventListener
的基础上进行封装,我们先来想想在此基础上需要什么?
首先,useEventListener
的入参可分为三个
第一个
event
是事件(如:click、keydown)第二个回调函数(所以不需要出参)
第三个就是目标(是某个节点还是全局)
在这里需要注意一点就是在销毁的时候需要移除对应的监听事件
代码
import { useEffect } from 'react';
const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) => {
useEffect(() => {
const targetElement = 'current' in target ? target.current : window;
const useEventListener = (event: Event) => {
return handler(event)
}
targetElement.addEventListener(event, useEventListener)
return () => {
targetElement.removeEventListener(event, useEventListener)
}
}, [event])
};
export default useEventListener;
注:这里把target
默认设置成了window
,至于为什么要这么写:'current' in target
是因为我们用useRef
拿到的值都是 ref.current
优化
接下来我们一起来看看如何优化这个组件,这里的优化与 useCreation
类似,但又有不同,原因是这里的需要判断的要比useCreation
复杂一点。
再次强调一下,传递过来的值,优先考虑使用
useRef
,再考虑用useState
,可以直接使用useLatest
,防止拿到的值不是最新值
这里简单说一下我的思路(又不对的地方或者有更好的建议欢迎评论区指出):
首先需要
hasInitRef
来存储是否是第一次进入,通过它来判断初始化存储然后考虑有几个参数需要存储,从上述代码上来看,可变的变量有两个,一个是
event
,另一个是target
,其次,我们还需要存储对应的卸载后的函数
,所以存储的变量应该有3个
接下来考虑一下什么情况下触发更新,也就是可变的两个参数:
event
和target
最后在卸载的时候可以考虑使用
useUnmount
,并执行存储对应的卸载后的函数
和把hasInitRef
还原
详细代码
import { useEffect } from 'react';
import type { DependencyList } from 'react';
import { useRef } from 'react';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
const depsAreSame = (oldDeps: DependencyList, deps: DependencyList):boolean => {
for(let i = 0; i < oldDeps.length; i++) {
if(!Object.is(oldDeps[i], deps[i])) return false
}
return true
}
const useEffectTarget = (effect: () => void, deps:DependencyList, target: any) => {
const hasInitRef = useRef(false); // 一开始设置初始化
const elementRef = useRef<(Element | null)[]>([]);// 存储具体的值
const depsRef = useRef<DependencyList>([]); // 存储传递的deps
const unmountRef = useRef<any>(); // 存储对应的effect
// 初始化 组件的初始化和更新都会执行
useEffect(() => {
const targetElement = 'current' in target ? target.current : window;
// 第一遍赋值
if(!hasInitRef.current){
hasInitRef.current = true;
elementRef.current = targetElement;
depsRef.current = deps;
unmountRef.current = effect();
return
}
// 校验变值: 目标的值不同, 依赖值改变
if(elementRef.current !== targetElement || !depsAreSame(deps, depsRef.current)){
//先执行对应的函数
unmountRef.current?.();
//重新进行赋值
elementRef.current = targetElement;
depsRef.current = deps;
unmountRef.current = effect();
}
})
useUnmount(() => {
unmountRef.current?.();
hasInitRef.current = false;
})
}
const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) => {
const handlerRef = useLatest(handler);
useEffectTarget(() => {
const targetElement = 'current' in target ? target.current : window;
// 防止没有 addEventListener 这个属性
if(!targetElement?.addEventListener) return;
const useEventListener = (event: Event) => {
return handlerRef.current(event)
}
targetElement.addEventListener(event, useEventListener)
return () => {
targetElement.removeEventListener(event, useEventListener)
}
}, [event], target)
};
export default useEventListener;
在这里只用
useEffect
是因为,在更新和初始化的情况下都需要使用必须要防止没有
addEventListener
这个属性的情况,监听的目标有可能没有加载出来
验证
验证一下useEventListener
是否能够正常的使用,顺变验证一下初始化、卸载的,代码:
import React, { useState, useRef } from 'react';
import { useEventListener } from '@/components'
import { Button } from 'antd-mobile';
const Index:React.FC<any> = (props)=> {
const [count, setCount] = useState<number>(0)
const [flag, setFlag] = useState<boolean>(true)
const [key, setKey] = useState<string>('')
const ref = useRef(null);
useEventListener('click', () => setCount(v => v +1), ref)
useEventListener('keydown', (ev) => setKey(ev.key));
return (
<div style={{padding: 20}}>
<Button color='primary' onClick={() => {setFlag(v => !v)}}>切换 {flag ? 'unmount' : 'mount'}</Button>
{
flag && <div>
<div>数字:{count}</div>
<button ref={ref} >加1</button>
<div>监听键盘事件:{key}</div>
</div>
}
</div>
);
}
export default Index;
效果:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
我们可以利用useEventListener
这个钩子去封装其他钩子,如 鼠标悬停,长按事件,鼠标位置等,在这里在举一个鼠标悬停的小例子
小例子 useHover
useHover:监听 DOM 元素是否有鼠标悬停
这个就很简单了,只需要通过 useEventListener
来监听mouseenter
和mouseleave
即可,在返回布尔值就行了:
import { useState } from 'react';
import useEventListener from '../useEventListener';
interface Options {
onEnter?: () => void;
onLeave?: () => void;
}
const useHover = (target:any, options?:Options): boolean => {
const [flag, setFlag] = useState<boolean>(false)
const { onEnter, onLeave } = options || {};
useEventListener('mouseenter', () => {
onEnter?.()
setFlag(true)
}, target)
useEventListener('mouseleave', () => {
onLeave?.()
setFlag(false)
}, target)
return flag
};
export default useHover;
效果:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
案例3: 有关时间的Hooks
在这里主要介绍有关时间的三个hooks,分别是:useTimeout
、useInterval
和useCountDown
useTimeout
useTimeout:一段时间内,执行一次
传递参数只要函数和延迟时间即可,需要注意的是卸载的时候将定时器清除下就OK了
详细代码:
import { useEffect } from 'react';
import useLatest from '../useLatest';
const useTimeout = (fn:() => void, delay?: number): void => {
const fnRef = useLatest(fn)
useEffect(() => {
if(!delay || delay < 0) return;
const timer = setTimeout(() => {
fnRef.current();
}, delay)
return () => {
clearTimeout(timer)
}
}, [delay])
};
export default useTimeout;
效果展示:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
useInterval
useInterval: 每过一段时间内一直执行
大体上与useTimeout
一样,多了一个是否要首次渲染的参数immediate
详细代码:
import { useEffect } from 'react';
import useLatest from '../useLatest';
const useInterval = (fn:() => void, delay?: number, immediate?:boolean): void => {
const fnRef = useLatest(fn)
useEffect(() => {
if(!delay || delay < 0) return;
if(immediate) fnRef.current();
const timer = setInterval(() => {
fnRef.current();
}, delay)
return () => {
clearInterval(timer)
}
}, [delay])
};
export default useInterval;
效果展示:
useCountDown
useCountDown:简单控制倒计时的钩子
跟之前一样我们先来想想这个钩子需要什么:
我们要做倒计时的钩子首先需要一个目标时间(targetDate),控制时间变化的秒数(interval默认为1s),然后就是倒计时完成后所触发的函数(onEnd)
返参就更加一目了然了,返回的是两个时间差的数值(time),再详细点可以换算成对应的天、时、分等(formattedRes)
详细代码
import { useState, useEffect, useMemo } from 'react';
import useLatest from '../useLatest';
import dayjs from 'dayjs';
type DTime = Date | number | string | undefined;
interface Options {
targetDate?: DTime;
interval?: number;
onEnd?: () => void;
}
interface FormattedRes {
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
const calcTime = (time: DTime) => {
if(!time) return 0
const res = dayjs(time).valueOf() - new Date().getTime(); //计算差值
if(res < 0) return 0
return res
}
const parseMs = (milliseconds: number): FormattedRes => {
return {
days: Math.floor(milliseconds / 86400000),
hours: Math.floor(milliseconds / 3600000) % 24,
minutes: Math.floor(milliseconds / 60000) % 60,
seconds: Math.floor(milliseconds / 1000) % 60,
milliseconds: Math.floor(milliseconds) % 1000,
};
};
const useCountDown = (options?: Options) => {
const { targetDate, interval = 1000, onEnd } = options || {};
const [time, setTime] = useState(() => calcTime(targetDate));
const onEndRef = useLatest(onEnd);
useEffect(() => {
if(!targetDate) return setTime(0)
setTime(calcTime(targetDate))
const timer = setInterval(() => {
const target = calcTime(targetDate);
setTime(target);
if (target === 0) {
clearInterval(timer);
onEndRef.current?.();
}
}, interval);
return () => clearInterval(timer);
},[targetDate, interval])
const formattedRes = useMemo(() => {
return parseMs(time);
}, [time]);
return [time, formattedRes] as const
};
export default useCountDown;
验证
import React, { useState } from 'react';
import { useCountDown } from '@/components'
import { Button, Toast } from 'antd-mobile';
const Index:React.FC<any> = (props)=> {
const [_, formattedRes] = useCountDown({
targetDate: '2022-12-31 24:00:00',
});
const { days, hours, minutes, seconds, milliseconds } = formattedRes;
const [count, setCount] = useState<number>();
const [countdown] = useCountDown({
targetDate: count,
onEnd: () => {
Toast.show('结束')
},
});
return (
<div style={{padding: 20}}>
<div> 距离 2022-12-31 24:00:00 还有 {days} 天 {hours} 时 {minutes} 分 {seconds} 秒 {milliseconds} 毫秒</div>
<div>
<p style={{marginTop: 12}}>动态变化:</p>
<Button color='primary' disabled={countdown !== 0} onClick={() => setCount(Date.now() + 3000)}>
{countdown === 0 ? '开始' : `还有 ${Math.round(countdown / 1000)}s`}
</Button>
<Button style={{marginLeft: 8}} onClick={() => setCount(undefined)}>停止</Button>
</div>
</div>
);
}
export default Index;
效果展示:
![outside_default.png](https://i-blog.csdnimg.cn/blog_migrate/35b9026e4bda3df223cecc228e5e9313.png)
End
参考
ahooks[5]
总结
简单的做下总结:
一个优秀的hooks一定会具备
useMemo
、useCallback
等api优化制作自定义hooks遇到传递过来的值,优先考虑使用
useRef
,再考虑用useState
,可以直接使用useLatest
,防止拿到的值不是最新值在封装的时候,应该将存放的值放入
useRef
中,通过一个状态去设置他的初始化,在判断什么情况下来更新所对应的值,明确入参与出参的具体意义,如useCreation
和useEventListener
盘点
本文一共讲解了12个自定义hooks,分别是:usePow
、useLatest
、useCreation
、useMount
、useUnmount
、useUpdate
、useReactive
、useEventListener
、useHover
、useTimeout
、useInterval
、useCountDown
这里的素材来源为ahooks,但与ahooks的不是完全一样,有兴趣的小伙伴可以结合ahooks
源码对比来看,自己动手敲敲,加深理解
相信在这篇文章的帮助下,各位小伙伴应该跟我一样对Hooks
有了更深的理解,当然,实践是检验真理的唯一标准,多多敲代码才是王道~
另外,觉得这篇文章能够帮助到你的话,请点赞+收藏一下吧,顺便关注下专栏,之后会输出有关React
的好文,一起上车学习吧~
react
其他好文:「React深入」这就是HOC,这次我终于悟了!!![6]
关于本文
来自:小杜杜
https://juejin.cn/post/7101486767336849421
声明:文章著作权归作者所有,如有侵权,请联系小编删除。
往期推荐
package.json 与 package-lock.json 的关系
最后
欢迎加我微信,拉你进技术群,长期交流学习...
欢迎关注「前端Q」,认真学前端,做个专业的技术人...
点个在看支持我吧
参考资料
[1]
https://juejin.cn/post/7088304364078497800: https://juejin.cn/post/7088304364078497800
[2]https://ahooks.js.org/zh-CN/hooks/use-creation: https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2Fzh-CN%2Fhooks%2Fuse-creation
[3]https://juejin.cn/post/7068935394191998990#heading-36: https://juejin.cn/post/7068935394191998990#heading-36
[4]https://juejin.cn/post/7088304364078497800#heading-82: https://juejin.cn/post/7088304364078497800#heading-82
[5]https://ahooks.js.org/zh-CN/hooks/use-request/index: https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2Fzh-CN%2Fhooks%2Fuse-request%2Findex
[6]https://juejin.cn/post/7103345085089054727: https://juejin.cn/post/7103345085089054727