react的hooks只要包括下面15个:
react16有10个
react18是5个
hooks解决的问题是:
- 为函数组件提供生命周期
- 为函数组件创建状态
- 提取函数组件的重复逻辑
根据作用划分
我们可以根据具体作用对他进行重新划分:处理状态的有8个,处理副作用的有3个,优化组件的有2个,工具hooks有2个。
一:状态hooks
1.1 useState ---v16
函数组件里面,只要props和state发生变化都会触发组件更新,useState是批量更新的,如果同一个状态,修改多次,只有最后一次起作用。
基本使用
const DemoState = (props) => {
/* number为此时state读取值 ,setNumber为派发更新的函数 */
let [number, setNumber] = useState(0) /* 0为初始值 */
return (<div>
<span>{ number }</span>
<button onClick={ ()=> {
setNumber(number+1)
console.log(number) /* 这里的number是不能够即使改变的 */
} } ></button>
</div>)
}
1.2 useReducer ---v16
1.必须在一个组件里面使用,不能跨组件。
2.适用于当修改state前需要执行大量重复逻辑的场景。
3.相当于是一个小型的redux
const DemoUseReducer = ()=>{
/* number为更新后的state值, dispatchNumbner 为当前的派发函数 */
const [ number , dispatchNumbner ] = useReducer((state,action)=>{
const { payload , name } = action
/* return的值为新的state */
switch(name){
case 'add':
return state + 1
case 'sub':
return state - 1
case 'reset':
return payload
}
return state
},0)
return <div>
当前值:{ number }
{ /* 派发更新 */ }
<button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
<button onClick={()=>dispatchNumbner({ name:'sub' })} >减少</button>
<button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >赋值</button>
{ /* 把dispatch 和 state 传递给子组件 */ }
<MyChildren dispatch={ dispatchNumbner } State={{ number }} />
</div>
}
1.3 useContext
1.子组件用useContext获取父组件传过来的context,主要的应用场景就是跨级传值。
2.它的弊端:如果在context
里面存储了a,b
两个变量,和setA,setB
的两个方法,把a
和setA
传入A
组件,把b
和setB
传入B
组件,当我用setA
修改a
变量的时候,即便B
组件没有使用 a
变量,但是B
组件也会被重新渲染。解决这个问题的办法就是把a
变量和b
变量拆分到不同的context
里面,这样又会出现大量的context.provide
的嵌套。陷入嵌套地狱!
3.综上所述,context只适用于场景比较简单的嵌套组件里面。关键时刻还得专门的状态管理器上场,比如redux,zustand,jotai。
/* 用useContext方式 */
const DemoContext = ()=> {
const value:any = useContext(Context)
/* my name is alien */
return <div> my name is { value.name }</div>
}
/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
return <Context.Consumer>
{/* my name is alien */}
{ (value)=> <div> my name is { value.name }</div> }
</Context.Consumer>
}
export default ()=>{
return <div>
<Context.Provider value={{ name:'alien' , age:18 }} >
<DemoContext />
<DemoContext1 />
</Context.Provider>
</div>
}
1.4 useRef
它用来存储dom,也可以用来存储那些不会引发重渲染的值,也就是说如果用ref存储了值,即使这个值发生了改变,他也不会导致组件重渲染。
const DemoUseRef = ()=>{
const dom= useRef(null)
const handerSubmit = ()=>{
/* <div >表单组件</div> dom 节点 */
console.log(dom.current)
}
return <div>
{/* ref 标记当前dom节点 */}
<div ref={dom} >表单组件</div>
<button onClick={()=>handerSubmit()} >提交</button>
</div>
}
1.5 useImperativeHandle
如果父组件给子组件传了一个ref
,在子组件里面可以将这个ref
指定给某个元素,这样父组件就能够轻松操控这个子组件里面的元素了。但是如果父组件想要操纵子组件的一些值或者是方法呢?此时就可以用useImperativeHandle
将子组件里面的一些值或者方法暴露出去,供父组件使用,这个hook
常常在组件封装的时候用到。
他接受三个参数,具体是:
- ① 第一个参数ref: 接受 forWardRef 传递过来的 ref。
- ② 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- ③ 第三个参数 deps : 依赖项 deps ,依赖项更改形成新的 ref 对象。
具体用法
function Son (props,ref) {
console.log(props)
const inputRef = useRef(null)
const [ inputValue , setInputValue ] = useState('')
useImperativeHandle(ref,()=>{
const handleRefs = {
/* 声明方法用于聚焦input框 */
onFocus(){
inputRef.current.focus()
},
/* 声明方法用于改变input的值 */
onChangeValue(value){
setInputValue(value)
}
}
return handleRefs
},[])
return <div>
<input
placeholder="请输入内容"
ref={inputRef}
value={inputValue}
/>
</div>
}
const ForwarSon = forwardRef(Son)
class Index extends React.Component{
inputRef = null
handerClick(){
const { onFocus , onChangeValue } =this.cur
onFocus()
onChangeValue('let us learn React!')
}
render(){
return <div style={{ marginTop:'50px' }} >
<ForwarSon ref={node => (this.inputRef = node)} />
<button onClick={this.handerClick.bind(this)} >操控子组件</button>
</div>
}
}
1.6 useSyncExternalStore
它主要是给redux和mobox这种状态管理器使用的,目的是要组件在concurrent模式下也能安全准确的拿到state。所以这个hook一般在开发的时候也用不到。
import { useSyncExternalStore } from "react";
import { Button } from "antd";
import { combineReducers, createStore } from "redux";
const reducer = (state: number = 1, action: any) => {
switch (action.type) {
case "ADD":
return state + 1;
case "DEL":
return state - 1;
default:
return state;
}
};
/* 注册reducer,并创建store */
const rootReducer = combineReducers({ count: reducer });
const store = createStore(rootReducer, { count: 1 });
const Index: React.FC<any> = () => {
//订阅
const state = useSyncExternalStore(
store.subscribe,
() => store.getState().count
);
return (
<>
<div>大家好,我是小杜杜,一起玩转Hooks吧!</div>
<div>数据源: {state}</div>
<Button type="primary" onClick={() => store.dispatch({ type: "ADD" })}>
加1
</Button>
<Button
style={{ marginLeft: 8 }}
onClick={() => store.dispatch({ type: "DEL" })}
>
减1
</Button>
</>
);
};
export default Index;
1.7 useTransition
启用过度任务,现实使用场景是:输入搜索框,当我们在input里面输入文本,然后在onchange的时候搜索数据的时候,一般会先用state创建一个loading设置loading是true,在页面上要他显示组件,等数据加载好以后就将loading改成false,然后驱动页面更新,显示加载后的数据。
有了useTransition以后,上面的loading就不要了,它会返回2个参数,一个是loading,一个是延迟执行函数。我们只需要将要做的事情加入延迟执行函数,他就能自己驱动loading发生改变。
import { useState, useTransition } from "react";
import { Input } from "antd";
const Index: React.FC<any> = () => {
const [isPending, startTransition] = useTransition();
const [input, setInput] = useState("");
const [list, setList] = useState<string[]>([]);
return (
<>
<div>大家好,我是小杜杜,一起玩转Hooks吧!</div>
<Input
value={input}
onChange={(e) => {
setInput(e.target.value);
startTransition(() => {
const res: string[] = [];
for (let i = 0; i < 10000; i++) {
res.push(e.target.value);
}
setList(res);
});
}}
/>
{isPending ? (
<div>加载中...</div>
) : (
list.map((item, index) => <div key={index}>{item}</div>)
)}
</>
);
};
export default Index;
1.8 useDeferredValue
useDeferredValue 用来包裹延迟的值,作用和useTransion一样,但是用法不同,一个是延迟执行函数,一个是延迟渲染值。
import { useState, useDeferredValue } from "react";
import { Input } from "antd";
const getList = (key: any) => {
const arr = [];
for (let i = 0; i < 10000; i++) {
if (String(i).includes(key)) {
arr.push(<li key={i}>{i}</li>);
}
}
return arr;
};
const Index: React.FC<any> = () => {
//订阅
const [input, setInput] = useState("");
const deferredValue = useDeferredValue(input);
console.log("value:", input);
console.log("deferredValue:", deferredValue);
return (
<>
<div>大家好,我是小杜杜,一起玩转Hooks吧!</div>
<Input value={input} onChange={(e: any) => setInput(e.target.value)} />
<div>
<ul>{deferredValue ? getList(deferredValue) : null}</ul>
</div>
</>
);
};
export default Index;
二. 副作用
2.1 useEffect
它里面的代码异步执行。
如上在 useEffect 中做的功能如下:
- ① 请求数据。
- ② 设置定时器,延时器等。
- ③ 操作 dom , 在 React Native 中可以通过 ref 获取元素位置信息等内容。
- ④ 注册事件监听器, 事件绑定,在 React Native 中可以注册 NativeEventEmitter 。
- ⑤ 还可以清除定时器,延时器,解绑事件监听器等。
知识点汇总
1.如果没有依赖值那就每次刷新都执行
2.如果依赖是[],只有页面挂载的时候执行一次
3.如果依赖数组里面有值,那么每次依赖值发生变化的时候才更新
4.它里面有个return出去的函数是组件卸载前要执行的
/* 模拟数据交互 */
function getUserInfo(a){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({
name:a,
age:16,
})
},500)
})
}
const Demo = ({ a }) => {
const [ userMessage , setUserMessage ] :any= useState({})
const div= useRef()
const [number, setNumber] = useState(0)
/* 模拟事件监听处理函数 */
const handleResize =()=>{}
/* useEffect使用 ,这里如果不加限制 ,会是函数重复执行,陷入死循环*/
useEffect(()=>{
/* 请求数据 */
getUserInfo(a).then(res=>{
setUserMessage(res)
})
/* 定时器 延时器等 */
const timer = setInterval(()=>console.log(666),1000)
/* 操作dom */
console.log(div.current) /* div */
/* 事件监听等 */
window.addEventListener('resize', handleResize)
/* 此函数用于清除副作用 */
return function(){
clearInterval(timer)
window.removeEventListener('resize', handleResize)
}
/* 只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount */
},[ a ,number ])
return (<div ref={div} >
<span>{ userMessage.name }</span>
<span>{ userMessage.age }</span>
<div onClick={ ()=> setNumber(1) } >{ number }</div>
</div>)
}
2.2 useLayoutEffect
它里面的代码同步执行。所以不会出现闪屏
① 首先 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。
② useLayoutEffect 的回调函数中代码执行会阻塞浏览器绘制。
如果你使用useEffect去操作state更新页面,发现页面有闪动,你就可以使用useLayoutEffect进行优化,但是useLayoutEffect会阻塞页面更新。因为他是同步执行的。
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
/*我们需要在dom绘制之前,移动dom到制定位置*/
const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
animate(target.current,{ x,y })
}, []);
return (
<div >
<span ref={ target } className="animate"></span>
</div>
)
}
2.3 useInsertionEffect
它的执行时机是dom更新前,一般我们都不会使用它,具体的使用场景是:为了解决css-in-js在渲染的过程中注入样式,会导致页面重绘问题。所以有了这个hook,我们就可以在dom更新前,把它的样式全部注入进去,就不会出现页面重绘问题了。
import { useInsertionEffect } from "react";
const Index: React.FC<any> = () => {
useInsertionEffect(() => {
const style = document.createElement("style");
style.innerHTML = `
.css-in-js{
color: blue;
}
`;
document.head.appendChild(style);
}, []);
return (
<div>
<div className="css-in-js">大家好,我是小杜杜,一起玩转Hooks吧!</div>
</div>
);
};
export default Index;
总结看看他们的真实的效果
import { useEffect, useLayoutEffect, useInsertionEffect } from "react";
const Index: React.FC<any> = () => {
useEffect(() => console.log("useEffect"), []);
useLayoutEffect(() => console.log("useLayoutEffect"), []);
useInsertionEffect(() => console.log("useInsertionEffect"), []);
return <div>大家好,我来了!</div>;
};
export default Index;
三. 性能优化
3.1 useMemo对比useCallback
useMemo是用来缓存值的hook,useCallback是用来缓存函数的hook。
一般在父组件里面嵌套子组件,如果父组件更新了,子组件就会自动更新。
为了节能,我们想要父组件更新了,如果子组件的props
没有发生变化,他就可以不用更新。
此时你可以用React.memo()
包裹组件。但是如果父组件给子组件传入的是函数或者是一个对象,你会发现在父组件每刷新一次,子组件也会刷新一次,即使函数表面上并没有发生变化。
这是因为当父组件刷新的时候,函数在内存里面的地址发生了变化,导致React.memo()的比较结果是值发生了变化。
如何解决这个问题呢?就需要useMemo
和useCallback
上场了,用他们把值和函数包裹住,保证,父组件刷新了,只要依赖值不变,函数就不会发生变化,传入子组件以后,子组件就不会更新。
import { memo, useCallback, useEffect, useMemo, useState } from "react";
function Aaa() {
const [,setNum] = useState(1);
const [count, setCount] = useState(2);
useEffect(() => {
setInterval(()=> {
setNum(Math.random());
}, 2000)
},[]);
const bbbCallback = useCallback(function () {
// xxx
}, []);
const count2 = useMemo(() => {
return count * 10;
}, [count]);
return <div>
<MemoBbb count={count2} callback={bbbCallback}></MemoBbb>
</div>
}
interface BbbProps {
count: number;
callback: Function
}
function Bbb(props: BbbProps) {
console.log('bbb render');
return <h2>{props.count}</h2>
}
const MemoBbb = memo(Bbb);
export default Aaa;
四.工具 hooks
4.1 useDebugValue
useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。这个hooks目的就是检查自定义hooks。 你可以理解成他就是一个用来替换console.log帮你调试的hook。
用法:
import { useState, useDebugValue } from 'react';
function useTestDebug(data: any) {
useDebugValue("我是snow来了");
useDebugValue(data);
return data;
}
export default function MyApp() {
const [count, setCount] = useState(0);
function countClick() {
setCount(c => c + 1);
}
useTestDebug(count);
return <div>
count: {count}
<button onClick={countClick}>count + 1</button>
</div>;
}
测试:
4.2 useId
当ReactSSR重构项目以后,启动项目,你会发现当服务端渲染一次以后,由于有些组件的id是用随机数获取到的,导致服务端渲染和浏览器渲染产生的id不同,即使浏览器加载的组件其他地方和服务器加载的组件一样,它依旧会因为id不同而重新渲染。这就会造成性能的消耗。为了解决这个问题,react专门提供了一个hooks,帮助组件不管是服务器渲染,还是浏览器渲染,都只有一个id。
const id = useId()
return (
<>
<label htmlFor={id}>名字:</label>
<input id={id} type="text" />
</>
)