最近一段时间一直开发react,虽然对框架层面没什么大的见解,但是对hooks的使用增多,这里记录一下常用hooks以便查阅。
因为接触react的时候,react已经出到18了,本文主要以函数式组件 react hooks来进行记录(无类组件相关)。
参考文章
1. useState
在函数组件中,可以使用useState来定义函数组件的状态。使用useState来创建状态
javascript
import React,{ useState } from 'react'
function StateFunction () {
const [name, setName] = useState('函数')
// 类名,修改函数名 初始值
return (
<div onClick={ () => setName('我使用hooks变成这样了') }>
// setName也可以写入方法,如setName( val => val+'xxxx' )
这是一个函数式组件------------{name}
</div>
)
}
export default StateFunction
2. useEffect
useEffect又称副作用hooks。作用:给没有生命周期的组件,添加结束渲染的信号。执行时机:在渲染结束之后执行
什么是副作用?
副作用 ( side effect ): 数据获取,数据订阅,以及手动更改 React 组件中的 DOM 都属于副作用
因为我们渲染出的页面都是静态的,任何在其之后的操作都会对他产生影响,所以称之为副作用
使用:
1.第一个参数,接收一个函数作为参数
2.第二个参数,接收【依赖列表】,只有依赖更新时,才会执行函数
3.返回一个函数,先执行返回函数,再执行参数函数
javascript
// 第一次渲染完成之后和每次更新渲染页面的时候,都会调用useEffect的回调函数
// 模拟 componentDidUpdate, 类比vue的updated
import React,{ useEffect, useState } from 'react'
function StateFunction () {
const [num, setNum] = useState(0)
useEffect( () => {
console.log('2222函数式组件结束渲染')
})
return (
<div onClick={ () => setNum( num => num+1 ) }>
这是一个函数式组件------------{num}
</div>
)
}
javascript
// 空数组,只运行一次,
// 模拟 React的componentDidMount, Vue的mounted,Vue3的onMounted
useEffect( () => {
console.log('2222函数式组件结束渲染')
},[])
// 改变useEffect第二个参数,其余代码同上
javascript
// 只有当数组中的值(任意一项即可)改变时,才会重新触发回调函数
// 模拟 componentDidUpdate 类比 vue的watch
useEffect( () => {
console.log('2222函数式组件结束渲染')
},[num])
useEffect( () => {
console.log('2222函数式组件结束渲染')
},[num,val])
// 改变useEffect第二个参数,其余代码同上
javascript
// useEffect中清除监听、延时函数setTimeout等操作
useEffect( () => {
console.log('2222函数式组件结束渲染')
const updateMouse = (e) => {
console.log('打印当前位置')
setPositions({ x:e.clientX, y:e.clientY })
}
document.addEventListener('click',updateMouse) // 添加绑定方法事件(要修改依赖,绑定到依赖上)
return () => {
// 在每次执行useEffect之前都会执行上一次return中内容
document.removeEventListener('click',updateMouse)
// 移除绑定方法事件(要修改依赖,绑定到依赖上)
console.log('1111销毁')
}
})
3. useLayoutEffect
一般将useLayoutEffect称为有DOM操作的副作用hooks。作用是在DOM更新完成之后执行某个操作。执行时机:在DOM更新之后执行
与useEffect对比
相同点
1.第一个参数,接收一个函数作为参数
2.第二个参数,接收【依赖列表】,只有依赖更新时,才会执行函数
3.返回一个函数,先执行返回函数,再执行参数函数
(所以说执行过程的流程是一样的)
不同点
执行时机不同。useLayoutEffect在DOM更新之后执行;useEffect在render渲染结束后执行。执行示例代码会发现useLayoutEffect永远比useEffect先执行,这是因为DOM更新之后,渲染才结束或者渲染还未结束
javascript
const [num, setNum] = useState(0)
//在类组件中用componentWillMount生命周期来实现, vue beforeMount
useLayoutEffect( () => {
console.log('useLayoutEfffect')
// 也可以在此进行事件绑定
return () => {
// 也可以在此进行事件绑定移除
console.log(1)
}
},[num])
useEffect( () => {
console.log('useEffect')
},[num])
return (
<div onClick={ () => setNum( num => num+1 ) }>
这是一个函数式组件------------{num}
</div>
)
4. useMemo
使用useMemo可以传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。简单来说,作用是让组件中的函数跟随状态更新(即优化函数组件中的功能函数)。
使用:
1.接收一个函数作为参数
2.同样接收第二个参数作为依赖列表(可以与useEffect、useLayoutEffect进行对比学习)
3.返回的是一个值。返回值可以是任何,函数、对象等都可以
javascript
// 未优化前代码如下。当我们点击div区域时,此时触发的setAge,改变的是age,跟getDoubleNum方法其实是不相关的,但是如果你看下控制台,能看到打印了多次获取双倍Num,说明该方法不断被触发,其实是没必要触发的。如果方法内计算量大、对性能是有一定影响的,所以需要进行优化
const [num, setNum] = useState(1)
const [age, setAge] = useState(18)
function getDoubleNum () {
console.log(`获取双倍Num${num}`)
return 2 * num // 假设为复杂计算逻辑
}
return (
<div onClick={ () => { setAge( age => age+1 )} }>
<br></br>
这是一个函数式组件------------{ getDoubleNum() }
<br></br>
age的值为------------{ age }
<br></br>
</div>
)
javascript
// 使用useMemo优化后代码如下。此时getDoubleNum方法是接收一个返回的值,所以要注意注释里所写的,括号是去掉了的。使用useMemo后,再点击div区域改变age的值,此时执行返回的return 2*num以及打印只有在num更新时才会去执行,然后返回值给到getDoubleNum再渲染到视图上,这样就减少了不必要的计算达到优化的作用
const [num, setNum] = useState(1)
const [age, setAge] = useState(18)
const getDoubleNum = useMemo( () => {
console.log(`获取双倍Num${num}`)
return 2 * num // 假设为复杂计算逻辑
},[num] )
return (
<div onClick={ () => { setAge( age => age+1 ) } }>
<br></br>
这是一个函数式组件------------num:{ getDoubleNum } // 注意这里没括号,因为是返回值
<br></br>
age的值为------------{ age }
<br></br>
</div>
)
看起来useEffect和useMemo参数一致,那么区别呢:
useMemo
传入useMemo的函数会在渲染期间执行,memo是在DOM更新前触发的,就像官方所说的,类比生命周期就是shouldComponentUpdate。
useEffect
useEffect只能在DOM更新后并且浏览器渲染完成后再触发,俗称"马后炮"。
javascript
// 未优化前代码如下。子组件包裹一个memo,但是包裹了还是会重新渲染, 为什么呢?因为我们定义的info是const定义的一个局部变量,每次重新渲染都是重新定义一个新的info,然后子组件进行浅层比较时候,info永远是不一样的,所以就会重新渲染(可以按照例子点击按钮,会发现子组件不断打印我是子组件)。如果子组件比较复杂的情况下,那么就会对页面性能产生影响
const Child = memo( () => {
console.log('我是子组件')
return <p>我是子组件</p>
})
function Parent() {
const [show,setShow] = useState(true)
const info = {
name: 'Even',
age: 22
}
return(
<div>
<Child info={ info } />
<button onClick={ () => setShow(!show) }>点击更新状态</button>
</div>
)
}
javascript
// 子组件只会在初始化状态时渲染一次,当我们点击按钮时,因为info其包裹的useMemo依赖并没有改变,返回值是同一个值,所以不会造成子组件重新渲染
const info = useMemo( () => {
return {
name: 'Even',
age: 22
}
},[])
5. useCallback
useMemo(() => Fn,deps)相当于useCallback(Fn,deps)
不同点:
useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以
相同点:
在使用方法上,useMemo与useCallback相同。接收一个函数作为参数,也同样接收第二个参数作为依赖列表
javascript
// getDoubleNum传入子组件,此时点击div区域改变的是num的值,我们使用父组件useCallback配合子组件的useEffect来优化,只有当父组件的num改变导致传入子组件的getDoubleNum改变的时候,我们才会执行子组件某些需要更新的操作(即注释标注处代码),这样就可以避免子组件一些没必要的更新操作反复执行而影响页面性能
function Parent () {
const [num, setNum] = useState(1)
const [age, setAge] = useState(18)
const getDoubleNum = useCallback( () => {
console.log(`获取双倍Num${num}`)
return 2 * num
},[num] )
return (
<div onClick={ () => {setNum( num => num+1 )} }>
这是一个函数式组件------------num:{ getDoubleNum() }
<br></br>
age的值为------------age:{ age }
<br></br>
set.size:{set.size}
<Child callback={ getDoubleNum() }></Child>
</div>
)
}
function Child(props) {
useEffect( () => {
console.log('callback更新了') //这里代表的是需要跟随传入内容的改变而同步进行的操作
},[props.callback])
return (
<div>
子组件的getDoubleNum{props.callback}
</div>
)
}
6. useRef
简单来说useRef就是返回一个子元素索引,此索引在整个生命周期中保持不变。作用也就是:长久保存数据。注意事项,保存的对象发生改变,不通知。属性变更不会重新渲染
javascript
// 此时以下的代码其实是没有办法完成给出的需求的,当num大于10后,会发现不停的打印大于10了,清除定时器,而其实是定时器没有清除掉的,所以会一直执行这两个打印内容,但是会发现打印出来的timer显示undefined,这是为什么呢?因为我们每次渲染都是通过setInterval重新返回的timer,timer也在更新,也就丢失了timer这个数据,导致无法准确清除某个需要清除的定时器
const [num, setNum] = useState(0)
let timer
useEffect( () => {
timer = setInterval( () => {
setNum( num => num+1 )
},400 )
},[] )
useEffect( () => {
if(num > 10){
console.log('大于10了,清除定时器')
console.log('timer:',timer)
// 因为每一个timer都是独立render的,所以获取不到
clearTimeout(timer)
}
},[num] )
return (
<div>
这是一个函数式组件------------num:{ num }
</div>
)
javascript
// num自增到11后就打印了一次大于10了,清除定时器以及ref.current 1,然后就停止自增了,因为定时器被清除了。ref是一个对象,ref.current存储了该定时器在整个生命周期中的id值,所以当清除定时器的时候,可以准确清除这个定时器
const [num, setNum] = useState(0)
const ref = useRef()
useEffect( () => {
ref.current = setInterval( () => {
setNum( num => num+1 )
},400 )
// ref.current = '111'
},[] )
useEffect( () => {
if(num > 10){
console.log('大于10了,清除定时器')
console.log('ref.current',ref.current)
clearTimeout(ref.current)
}
},[num] )
return (
<div>
这是一个函数式组件------------num:{ num }
</div>
)
7. useContext
useContext是让子组件之间共享父组件传入的状态的。作用通俗地说是带着子组件去流浪。
javascript
// 未使用useContext,我们有下列这样一个场景,我们父组件有传入一个值到不同的子组件中,示例给出的代码是2个这样的子组件,但是如果我需要添加的子组件特别多呢?总不能总是一个一个这样添加写入吧,而且如果传入的同一个变量名如果发生改变,还得一个个去改
function StateFunction () {
const [num, setNum] = useState(1)
return (
<div>
<button onClick={ ()=> setNum(num => num+1) }>增加num的值+1</button>
<br></br>
这是一个函数式组件------num:{ num }
<Item1 num={num}></Item1>
<Item2 num={num}></Item2>
// ......
</div>
)
}
function Item1 (props) {
return (
<div>
子组件1 num:{ props.num }
</div>
)
}
function Item2 (props) {
return (
<div>
子组件2 num:{ props.num }
</div>
)
}
javascript
// 需要引入useContetx,createContext两个内容
// 通过createContext创建一个context句柄
// Context.Provider来确定数据共享范围
// 通过value来分发内容
// 在子组件中,通过useContext(Context句柄)来获取数据
// 注意事项,上层数据发生改变,肯定会触发重新渲染(点击button按钮触发父组件更新传入的num值能看到子组件重新渲染)
const Context = createContext(null)
function StateFunction () {
const [num, setNum] = useState(1)
return (
<div>
<button onClick={ ()=> setNum(num => num+1) }>增加num的值+1</button>
<br></br>
这是一个函数式组件------num:{ num }
<Context.Provider value={num}>
<Item3></Item3>
<Item4></Item4>
</Context.Provider>
</div>
)
}
function Item3 () {
const num = useContext(Context)
return (
<div>
子组件3: { num }
</div>
)
}
function Item4 () {
const num = useContext(Context)
return (
<div>
子组件4: { num+2 }
</div>
)
}
以下是ahooks常用方法
1. useRequest
useRequest 的第一个参数 service 是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 loading , data , error 等状态。
useRequest 是自定触发请求的,也就说当组件挂载到 DOM 树上后,请求会立即发出。如果我们希望手动触发请求,可以为 useRequest 函数配置第二个参数。
第二参数是一个 options 配置,如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 来触发执行。
javascript
import {useRequest } from 'ahooks';
const getSome = async () => {};
const { data, loading, run } = useRequest(getSome, {
debounceInterval: 500,
manual: true,
refreshDeps: [], // manual为false,可以按被动式触发
});
<div>
提交结果:{JSON.stringify(data)}
</div>
<Button loading={loading} onClick={run}>
提交
</Button>
js
const { data, runAsync: runGetData } = useRequest(getStationList, {
manual: true,
});
const {
loading: runUpdateStationOwnerApiLoading,
run: runUpdateStationOwnerApi,
} = useRequest(updateStationOwnerApi, {
manual: true,
onSuccess() {
message.success(2222);
setVisible(false);
},
});
const { runAsync: runDeleteStation } = useRequest(deleteStation, {
manual: true,
onSuccess: res => {
if (res === true) {
message.success(3333);
} else {
message.error(2222);
}
},
});
// 不手动 reject 会一直 loading
// eslint-disable-next-line @typescript-eslint/no-empty-function
return runGetData({ regionCode, ...queryParams }).catch(res => res);
runUpdateStationOwnerApi({
stationId: currentStationData.stationId,
stationOwner,
});
<Modal
key={2223}
visible={visible}
title={222}
onCancel={() => setVisible(false)}
destroyOnClose
formProps={{ initialValues }}
loading={runUpdateStationOwnerApiLoading}
onOk={() => setVisible(false)}
customizeForm={
<ModalFormItem
name="owner"
label={111}
rules={[{ required: true, message: 111 }]}
>
</ModalFormItem>
}
/>
operations.push({
key: 'del',
label: 'delete',
popConfirmConfig: {
title: 2222,
onConfirm: () => {
return runDeleteStation({ data });
},
},
});
2. useCreation
代替useMemo、useRef的钩子
(创建不希望因为render导致,函数重新执行的方法,采用useCreation)
因为useMemo不能保证一定不会被重新计算
useRef如果针对复杂对象,每次渲染都创建一次会很耗性能
javascript
import { useCreation } from 'ahooks';
const foo = useCreation(() => {number: Math.random()}, []);
3. useMemoizedFn
代替useCallback的钩子
在 React 中,useMemoizedFn 和 useCallback 都用于优化函数组件的性能,但它们有一些不同点。
理论上,可以使用 useMemoizedFn 完全代替 useCallback。区别在于memoizedFn不用自己指定依赖
javascript
import { useCreation } from 'ahooks';
const foo = useCreation(() => {number: Math.random()}, []);
4. useSafeState
useSafeState,使用安全状态挂钩以防止在未安装的组件上设置状态,以防止在未安装的组件上设置状态时发生的内存泄漏
快速点击切换tab栏,加载组件后会调用接口获取数据、设置state,如果这时候接口还没返回,组件就卸载了,没state可设置了,就会导致内存泄漏
javascript
import { useSafeState} from 'ahooks';
const [state,setState] = useSafeState(0);
<p> state.count:{state.count}</p>
<button onClick={() => setState(state=>state++)}>
state
</button>
5. useMount,useUnmount
useMount,在组件首次渲染时执行
useUnmount,组件卸载时执行的 Hook,比如组件卸载时,需要清楚定时器或者相关的监听,就可以使用useUnmount。