Hook 常用Api系列:
1.useState
它与class组件里面的 this.state 提供的功能完全相同
useState接收一个初始值,这个初始值可以是对象,也可以是简单数据类型。setState 函数用于更新状态。它接受一个新的状态值,并排队等待重新渲染该组件。
先贴上一个我自己手写的useState粗糙源码,帮助大家更好的理解:
/*
首先,我们需要在外部声明一个state空数组和下标
作用是再函数调用时,值不会被刷新
*/
let state=[];
let index=0;
//useState函数 //默认值
function myUseState(initialValue) {
let currentIndex=index; //引入中间变量currentIndex就是为了保存当前操作的下标index。
state[currentIndex] = state[currentIndex]===undefined? initialValue:state[currentIndex]; //判断,给state存入值
//将state对应下标的值进行更新
const setState = (newValue) => {
state[currentIndex] = newValue;
render();
};
index+=1; //如果有多个myUseState,每次更新完state值后,index值+1
//返出为数组
//第一个参数是当前值,第二个是赋值后的返出的值
return [state[currentIndex], setState];
}
//render渲染,选然后初始化index
const render = () => {
index=0; //重要的一步,必须在渲染前后将index值重置为0,不然index会一种增加1
ReactDOM.render(<App />, document.getElementById("root"));
};
// 使用myUseState
const App = () => {
const [num, setNum] = myUseState(0);
const [age, setAge] = myUseState(0);
return (
<div classNam="App">
<p>num的值:{num}</p>
<button onClick={()=>{setNum(n+1)}}> 对Num+1</button>
<p>age的值:{Age}</p>
<button onClick={()=>{setAge(m+1)}}> 对Age的值1</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
有细心小伙伴会发现:App用了state和index,那其他组件用什么?放在全局作用域重名了怎么办?
这里有两个方法:
解决办法1: 每个组件都创建一个state和index。
解决办法2: 放在组件对应的虚拟节点对象上
注意: React的节点应该是FiberNode,state的真实名称为memorizedState,index的实现使用了链表
useState只有一个参数: 接收一个初始化状态的值(设置初始值),再第一次被组件调用时使用来作为初始化值(如果不设置则默认为undefined);
useState的返回值: 返回一个数组,数组包含两个元素;
元素一: 当前状态的值(第一次调用为初始化值);
元素二: 是一个设置状态值变化的函数;
不过我们如果总是使用索引来获取这两个元素总是不方便的, 因此在开发中我们通常是会对数组进行解构(当然要取什么名字是自定义的)
使用它们会有两个额外的规则:
·只能在函数组件的顶层调用 Hook。不能在循环语句、条件判断语句或者子函数中调用。
·只能在 React 的函数组件和自定义hook中调用 Hook。不能在其他 JavaScript 函数中调用。
2.useEffect && useLayoutEffect
useEffect 的回调函数是【异步宏任务】,在下一轮事件循环才会执行。根据 JS 线程与 GUI 渲染线程互斥原则,在 JS 中页面的渲染线程需要当前事件循环的宏任务与微任务都执行完,才会执行渲染线程,渲染页面后,退出渲染线程,控制权交给 JS 线程,再执行下一轮事件循环。
好处:这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的渲染更新。
坏处:页面会有闪烁,此时需要使用useLayoutEffect来解决这个问题(在下面),产生二次渲染问题,第一次渲染的是旧的状态,接着下一个事件循环中,执行改变状态的函数,组件又携带新的状态渲染,在视觉上,就是二次渲染。
如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做
·componentDidMount 组件挂载 当数组为空时useEffect只执行一次
·componentDidUpdate 组件更新 当数组不为空有监听对象时,挂载和更新useEffect都会执行
·componentWillUnmount 组件将要摧毁 当return一个处理逻辑时,useEffect在销毁组件时执行
这三个函数的组合。
useEffect传递两个参数,第一个参数是逻辑处理函数,第二个参数是一个数组
useEffect的第二个参数存放变量(可以不传,也不会报错,但是浏览器会无限循环执行处理逻辑的函数),当数组内存放变量发生改变时,第一个参数预设的逻辑处理函数将会被执行
useEffect((逻辑处理函数,数组(空包含了state)) => {
/** 执行逻辑 */
},[])
在第二个参数为空数组或者有变量的时候,会出现以下情况:
1.第二个参数如果时空数组,逻辑处理函数里面的逻辑只会在组件挂载时执行 一次 , 相当于 componentDidMount
2.第二个参数如果不为空数组,如下
const [age, setAge] = useState(1);
const [name, setNmae] = useState(2);
useEffect(() => {
/** 执行逻辑 */
},[age,name])
此时逻辑处理函数会在组件挂载时执行一次和(age或者name变量在栈中的值发生改变时执行一次) 就相当于componentDidMount 和 componentDidUpdate 的结合
第二个参数监听常见踩雷 :
1.useEffect执行函数里面改变了useEffect监测的变量:
逻辑处理的时候对监听的变量进行赋值操作,当监听元素的地址发生变化时,useEffect会再一次的执行,如此循环往复,形成无限循环咯
const [age, setAge] = useState(1);
useEffect(() => {
/** 执行逻辑 */
setAge(age+1)
},[age,name])
2.useEffect监测不到依赖数组/对象(引用类型)元素的变化:
const [a, setA] = useState({
name: 'ikun',
age: '18',
})
const changeA = () => {
//形参,表示a的值
setA((old) => {
old.name = 'lovekunkun'
return old
})
}
/**当changeA执行却没有打印 a*/
useEffect(() => {
/** 执行逻辑 */
console.log(a)
},[a])
原因是引用类型存放在堆内存内,而useEffect监听不到堆内存内的变化,所以会失效,所有的引用类型都需要注意这一点。但是可以通过 useReducer组合,实现引用类型的useEffect。
代码如下:
import { useEffect,useReducer } from "react";
//定义好obj
const obj = {
num: 1,
age: 2,
};
//定义reducer函数
function reducer(state, action) {
//解构action
const { num, age } = state;
//写好type返出
if (action.type === 'cheng') {
return { num: num *age, age };
} else if (action.type === 'age') {
console.log('pppp')
return { num, age: action.age };
} else {
throw new Error();
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, obj);
const { num, age } = state;
useEffect(() => {
//写好一个定时器
const id = setInterval(() => {
dispatch({ type: 'cheng' });
}, 1000);
//当销毁组件时,清除定时器
return () => clearInterval(id);
//我们直接监听dispa
}, [dispatch]);
return (
<>
<h1>{num}</h1>
<input value={age} onChange={e => {
dispatch({
type: 'age',
age: Number(e.target.value)
});
}} />
</>
);
}
这里我们采用了监听dispatch,而dispatch它是预先定义好的,不会更改,通过dispatch再去发送action,更新的逻辑就交给reducer去处理
useLayoutEffect
useLayoutEffect 与 componentDidMount、componentDidUpdate 生命周期钩子是【异步微任务】,在渲染线程被调用之前就执行。这意味着回调内部执行完才会更新渲染页面,没有二次渲染问题。
好处:没有二次渲染问题,页面视觉行为一致。
坏处:在回调内部有一些运行耗时很长的代码或者循环时,页面因为需要等 JS 执行完之后才会交给渲染线程绘制页面,等待时期就是白屏效果,即阻塞了渲染。
在用法上,我们只需要将useEffuct改成useLayoutEffect即可
3.useMemo
作用:
useMemo相当于vue的计算属性,只有当它所依赖的值发生变化的时候,useMemo才会重新返回一个新的值
应用场景:
常用于父子组件内,当父更新一个值传递到某个子组件内后,render会把其它挂载的组件也进行渲染,这就造成了不必要的性能开销,此时使用useMemo可以避免这个问题。
注意:
如果用useMemo返回的数据,只有对应的变量会发生变化,而其它的常量是不会变化的,所以如果要对useMemo的返回值进行一些splice操作的话,是无法起作用的,它无法改变其自己本身已经定义好的数据结构,改变的只是里面的变量。
useMemo的执行顺序为:
useMemo -> render -> useEffect
使用说明 :
同useEffect相同,包含两个参数,第一个是执行回调,第二个是监听的值
useMemo(() => {
}, []);
4.useCallBack
作用:
同useMemo相同,但是返出的是一个函数而不是值
应用场景:
监听一些如map,for一类的处理事件的值,避免无意义的开销
使用说明 :
import Title from './26Title'
import Button from './26Button'
function app(){
const [num, setNum] = useState(1);
const [age, setAge] = useState(1);
const incrementAge =useCallback(() => {
()=>{
setAge(age+999)
console.log("触发了incrementAge")
}
}, [age]);
const incrementSalary =useCallback(() => {
()=>{
setNum(num+100)
console.log("触发了incrementSalary ")
}
}, [num]);
return (
<>
<Title />
<Button
handleClick={incrementAge}
>incrementAge</Button>
<Button
handleClick={incrementSalary}
>incrementSalary</Button>
</>
);
}
}
Title
import React from 'react'
function Title() {
console.log('Rendering Title')
return (
<h2>useCallback</h2>
)
}
export default Title
Button
import React from 'react'
function Button(props: {
handleClick: () => void
children: string
}) {
console.log('Rendering button', props.children)
return (
<button onClick={props.handleClick}>
{props.children}
</button>
)
}
export default Button
当刷新进入页面后rending button会打印两次 reding title会打印一次,共计3次。
不使用useCallBack时,每次点击都是三个打印
使用useCallback后,只会显示一个reading button的打印。
5.useRef
useRef会返回一个可变的ref对象,它会把初始化参数绑定到current属性上。当然初始化属性可以通过函数返回。
useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref)
通过useRef保存的属性可以在渲染前后不发生变化,并且改变.current属性不会触发组件渲染!
使用场景
媒体播放:你还可以使用引用访问媒体资源,如图像、音频或视频,并与它们的渲染方式进行交互。例如,当元素进入视口时,自动播放视频或延迟加载图像。
复杂动画触发:传统上,CSS keyframes 或 timeout 用来确定何时启动动画。在某些情况下(可能更加复杂),可以使用 ref 来观察 DOM 元素并确定何时启动动画。
与 input 元素交互:通过使用引用,可以访问 input 元素并执行聚焦、变化跟踪或自动完成等功能。
与第三方 UI 库交互:ref 可用于与第三方 UI 库创建的元素交互,使用标准 DOM 方法访问这些元素可能比较困难。例如,如果你使用第三方库生成滑块,你可以使用 ref 来访问滑块的 DOM 元素,而不必知道滑块库的源代码结构