一 前言
1.1 Hooks的定义
- react推荐使用函数组件,但是有时需要使用state或其他功能时,只能在类组件中使用。因为函数组件没有实例,没有生命周期,只有类组件中才有。
1.2 React hooks解决什么问题
如果没有Hooks,函数组件能够做的只是接受 Props、渲染 UI ,以及触发父组件传过来的事件。所有的处理逻辑都要在类组件中写,这样会使 class 类组件内部错综复杂,每一个类组件都有一套独特的状态,相互之间不能复用。【组件之间的复用状态】
- 状态逻辑难复用:组件之间的复用状态逻辑很难,可能要用到render props(渲染属性)或者HOC(高阶组件),只能在原先的组件外包裹一层父容器,会导致层级冗余。
- this指向问题:父组件给子组件传递函数时,必须绑定this,导致性能上有损耗。
1.3 Hooks执行原理
Hooks是在fiber节点上存放了memorizedState链表,每个hook都从对应的链表元素上存取自己的值。
组件第一次被渲染时,为每个hook创建一个对象。
type Hook = {
memorizedState:any,
baseState:any,
baseUpdate:Update<any,any> | null,
queue:UpdateQueue<any,any> | null,
next:Hook | null
}
这个对象的memorizedState属性是用来存储组件上一次更新后的state,而next则指向下一个hook对象,最终形成一个链表。
二 hooks 之数据更新驱动
2.1 useState
useState使函数组件像类组件一样拥有state,函数组件通过useState可以让组件重新渲染,更新视图。
// 方法一:初始值为基础数据类型或者Object
const [ ①state , ②dispatch ] = useState(③initData)
// 方法二:初始值为函数
const [state,setState] = useState(()=> initialState)
①state,目的提供给UI,作为渲染视图的数据源
②dispatch改变state的函数
③initData有两种情况,第一种情况是非函数(作为state初始化的值)。第二种情况是函数,(函数返回值作为useState初始化的值)
2.1.1 useState对于复杂数据类型的写法
const [arr,setArr] = useState([1,2,3])
setArr([...arr,4]) // 末尾新增 扩展运算符
setArr([0,...arr]) // 头部新增 扩展运算符
setArr(arr.filter((item) => item !== 1) // 删除指定元素
setArr(arr.map(item => { return item == 2 ? 555 : item})) //替换数据
2.1.2 useState修改初始值对象的值
const [obj,setObj] = useState({k1:"v1",k2:"v2"})
<button onClick={() =>setObj(obj =>({...obj,k2:obj.k2 + 1}))}> + </button>
2.1.3直接更新和函数更新的区别
- 直接更新:在一次渲染中,无论调用多少次set函数,页面只会更新1次,相同的set函数只有最后一次调用会生效。
- 函数更新:页面只会渲染一次,每一次调用都能够获取到上一次的状态进行计算,react会将函数放入到一个队列中,在渲染前会依次执行函数。
<!---->
const [number,setNum] = useState(0)
/*直接改变状态*/
const increaseNumber = () =>{
/*页面渲染一次,累加一次,最后一次有效*/
setNum(number + 1)
setNum(number + 2)
setNum(number + 1)
}
/*函数更新*/
const increaseNumber = () =>{
/*页面渲染一次,累加3次,每一次都有效*/
setNum((number)=> number + 1)
setNum((number)=> number + 1)
setNum((number)=> number + 1)
}
2.1.4函数式更新(取最新值)
新的状态依赖于旧的状态,推荐使用函数式更新。因为状态更新可能是异步的。
setArr(prevCount => prevCount + 1)
2.1.5 useState异步回调获取不到最新值?
xiaoshen.blog.csdn.net/article/det…
在异步回调或闭包中获取最新状态并设置状态出现异常。
解决方案:
封装一个hooks将state和ref关联,提供一个方法供异步中获取最新值使用。
const useGetState = (initVal)=>{
const [state,setState] = useState(initVal)
const ref = useRef(initVal)
const setStateCopy = (newVal) =>{
ref.current = newVal
setState(newVal)
}
const getState = () =>ref.current
return [state,setStateCopy,getState]
}
const App =() => {
const [arr,setArr,getArr] = useGetState([0])
useEffect(()=>{
console.log(arr)
},[arr])
const handleClick = () =>{
Promise.resolve().then(()=>{
setArr([...getArr(),1])
})
.then(() =>{
setArr([...getArr(),2])
})
}
return (
<>
<button onClick={handleClick}>change</button>
<>
)
}
2.1.6 useState执行机制
- React17版本
- 组件生命周期或react合成事件中,是异步
- 在setTimeout或原生dom事件中,是同步
- react18版本
- 都是异步执行,提高性能(批量更新)
2.1.7 useState返回数组而非对象的原因
- 数组解构赋值允许自定义状态变量命名,而对象解构必须使用固定的属性名。
- 每次渲染时。useState返回的状态值和更新函数保持相同的索引位置,这样可以确保函数组件在渲染时访问到对应的状态值和更新函数。
2.2 useReducer(state比较复杂)
useReducer 是 react-hooks 提供的能够在无状态组件中运行的类似redux的功能 api 。
const [ ①state , ②dispatch ] = useReducer(③reducer,initialState,init?)
① 更新之后的 state 值。
② 派发更新的 dispatchAction 函数, 本质上和 useState 的 dispatchAction 是一样的。
③ reducer是一个函数 ,我们可以认为它就是一个 redux 中的 reducer , reducer的参数就是常规reducer里面的state和action, 返回改变后的state, 这里有一个需要注意的点就是:如果返回的 state 和之前的 state ,内存指向相同,那么组件将不会更新。
initialState是开始时的状态值
init是一个可选的初始化函数,用于延迟创建初始状态
2.2.1 useReducer使用场景
- state状态比较复杂
- 多个state状态之间有依赖关系
三 hooks之执行副作用
3.1 useEffect
useEffect,允许在函数组件中执行副作用的操作。副作用是指那些对外部世界产生影响的操作,例如数据获取、订阅、更改React组件之外的DOM等
使用useEffect:
useEffect(() =>{
// 执行副作用逻辑
},[/* 依赖列表*/])
- 1、
没有依赖(不传递第二个参数):如果不提供依赖数组,副作用函数在每次渲染后和所有更新后都会执行。
useEffect(() =>{
// 在每次组件渲染后都会执行
})
- 2 、
空依赖(空数组):如果依赖数组为空([]),副作用函数只会在组件挂载(mount)后执行一次,并在组件卸载时执行清理(如果提供了清理函数)
useEffect(()=>{
//只在组件挂载时执行一次
return() =>{
// 在组件卸载时执行清理
}
},[])
- 3、
具有依赖的useEffect:如果依赖数组中有值,首次挂载后执行,后续的每一次渲染后,React会使用Object.is()算法比较数组中每一个依赖项的当前值和上一次渲染时的值。【object.is()比较机制:原始类型,比较的是值;引用类型,比较的是引用地址】
useEffect(() =>{
// 依赖值改变时执行
},[dependency])
3.1.1 useEffect的原理
基于React组件的生命周期函数。当组件的props或state发生变更时,会触发一个更新循环。调用useEffect中的函数,根据组件中获取的变更信息执行useEffect中定义的操作。
3.1.2 useEffect的副作用执行是在React渲染的那个阶段?
useEffect 的副作用执行是在 React 完成 DOM 更新之后,也就是在浏览器的重新绘制(repaint)和重排(reflow)之后。
3.1.3 useEffect使用场景
- 初次加载页面(组件挂载)
- 响应式变量发生变化,触发页面根据新值重新渲染(组件更新)
- 关闭页面(组件卸载)
3.1.4 react18 useEffect执行两次?
在开发环境中除了必要的挂载之外,还“额外”模拟执行了一次组件的卸载和挂载
3.1.4.1让useEffect只执行一次【清理副作用】
/*清理事件监听------在返回函数内部"取消掉事件监听"即可*/
useEffect(()=>{
function handleScroll(e){}
window.addEventListener('scroll',handleScroll)
return ()=> window.removeEventListener('scroll',handleScroll)
},[])
/*重置页面数据,清理属性状态*/
useEffect(()=>{
const node.style.opacity = 1
return ()=>{node.style.opacity = 0}
},[])
3.1.5 useEffect闭包陷阱
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
// 问题:这里的count总是最初的值,即使setCount被调用
console.log(`Count: ${count}`);
}, 1000);
return () => clearInterval(intervalId);
}, []); // 依赖数组为空,意味着effect不会重新运行
}
由于useEffect的依赖数组是空的([]),所以定时器只在组件挂载时设置一次。然而,定时器内部的闭包捕获了count的初始值(0),即使在组件内部count改变了,定时器中使用的count仍然是初始值。
- 解决方案一:
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
console.log(`Count: ${count}`);
}, 1000);
return () => clearInterval(intervalId);
}, [count]); // 依赖数组包含count,因此每当count变化时,effect都会重新运行
}
通过将count添加到依赖数组中,每当count更新时,useEffect都会重新运行,从而正确地使用最新的count值。
- 解决方案二:使用函数和引用
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
const logCount = () => {
console.log(`Count: ${count}`);
};
logCount(); // 使用函数来确保使用的是最新的count值
}, 1000);
return () => clearInterval(intervalId);
}, [count]); // 仍然需要count在依赖数组中以确保正确的重新运行时机
}
3.1.6 useEffect依赖项是函数,如何处理
如果将一个函数作为依赖项,需要注意这个函数是如何定义的,因为它可能会导致无限循环或不必要的副作用执行。【当函数为依赖项时,每次组件更新渲染,如果该函数的内容发生变化,useEffect都会再次执行】
- 解决方案一:使用useCallback
import React, { useState, useEffect, useCallback } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
// 使用useCallback来确保只有在依赖项变化时才重新创建函数
const handleChange = useCallback(() => {
// 你的逻辑
console.log('Count changed:', count);
}, [count]); // 确保依赖项是最新的count
useEffect(() => {
// 使用handleChange作为依赖项
handleChange();
}, [handleChange]); // 这样handleChange只有在变化时才会导致副作用执行
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
- 解决方案二:直接在useEffect中使用匿名函数
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 直接在useEffect中定义匿名函数
const handleChange = () => {
console.log('Count changed:', count);
};
handleChange(); // 调用函数
}, [count]); // count作为依赖项,确保只有在count变化时才执行副作用
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
⚠️:如果将一个依赖于其自身副作用输出的函数作为依赖项,可能会导致无限循环 例如:
useEffect(() => {
const intervalId = setInterval(() => {
// 这里使用了intervalId,但将它作为依赖项可能导致无限循环
console.log('Interval tick');
}, 1000);
return () => clearInterval(intervalId); // 清理函数确保组件卸载时清除定时器
}, [intervalId]); // 错误!这将导致无限循环,因为intervalId依赖于其自身输出的值。
解决方法:不将定时器的ID作为依赖项,或使用useRef存储定时器ID
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
const intervalId = useRef(); // 使用useRef避免无限循环问题
const [count, setCount] = useState(0); // 示例状态变量,实际应用中可能不需要它来控制定时器。
useEffect(() => {
intervalId.current = setInterval(() => {
console.log('Interval tick');
setCount(prevCount => prevCount + 1); // 更新状态以显示效果(可选)
}, 1000);
return () => clearInterval(intervalId.current); // 清理函数确保组件卸载时清除定时器
}, []); // 不需要依赖项,因为我们不依赖于外部的state或props值。如果需要依赖于外部值(例如props),则不包括此空数组。但通常定时器不依赖于外部值。
return (
<div>Count: {count}</div> // 显示状态更新的效果(可选)
);
}
3.1.7 useEffect依赖项是对象或数组,如何处理?
如果依赖项是一个对象或数组,要确保依赖项的引用不变性。React将不会在对象的属性或数组的元素发生变化时重新运行副作用。因为React通过比较引用判断是否需要重新运行副作用。
- 解决方案一:使用useMemo包装对象或数组
import React, { useEffect, useMemo } from 'react';
function MyComponent() {
const [data, setData] = useState({});
const memoizedData = useMemo(() => {
// 这里可以执行一些计算或处理,返回一个新的对象或数组
return { ...data }; // 例如,返回data的一个深拷贝
}, [data]); // data变化时,memoizedData会更新
useEffect(() => {
// 副作用逻辑,使用memoizedData作为依赖项
console.log('副作用运行', memoizedData);
}, [memoizedData]); // memoizedData变化时,副作用会重新运行
return (
<div>
{/* 组件内容 */}
</div>
);
}
- 解决方案二:使用useRef,useRef可以创建一个可变的引用对象,其.current属性持有的值在组件的整个生命周期内保持不变,除非显式修改
import React, { useEffect, useRef } from 'react';
function MyComponent() {
const [data, setData] = useState({});
const savedData = useRef(data); // 初始值设置为data
useEffect(() => {
// 当data变化时,更新savedData.current的值
if (data !== savedData.current) {
savedData.current = data;
}
// 副作用逻辑,使用savedData.current作为依赖项
console.log('副作用运行', savedData.current);
}, [data]); // data变化时,副作用会重新运行
return (
<div>
{/* 组件内容 */}
</div>
);
}
3.1.8 useEffect 为啥不能直接使用异步async
异步函数(async函数)在JS中返回一个Promise对象,如果在useEffect中直接使用async函数,实际返回的是一个Promise.React中的useEffect期望返回清理函数或不返回。
useEffect(async () =>{
// ....
})
//等价于:
useEffect(()=>{
return new Promise(...)//违反React规则
})
3.1.8.1 useEffect正确使用async的方式
useEffect(() =>{
let isActive = true // 防止内存泄漏
const loadData = async () =>{
try {
const res = await fetch('/api')
} catch(err) {
console.log('失败',err)
}
}
loadData()
// 清理函数
return () =>{
isActive = false
}
},[/* 依赖项*/])
3.1.8.2 竞态条件,使用new AbortController()解决
3.2 useLayoutEffect(在浏览器重新绘制屏幕之前触发)
3.2.1使用场景
- 需要同步测量DOM元素
- 需要在视觉更新前进行DOM修改
- 需要避免闪烁或布局抖动
- 处理依赖于DOM布局的动画
3.3 useEffect与useLayoutEffect的区别
- 执行时机
- useEffect:在组件渲染到屏幕之后
异步执行,不会阻塞浏览器绘制 - useLayoutEffect:在所有DOM变更之后
同步执行,会阻塞浏览器绘制,直到完成
- useEffect:在组件渲染到屏幕之后
- 用途
- useEffect:用于异步操作(数据获取、订阅、事件监听)和不影响布局的副作用
- useLayoutEffect:用于需要同步执行的操作(读取DOM布局、同步DOM变更)和防止布局闪烁的副作用
3.4 useEffect与useLayoutEffect的执行顺序?
useLayoutEffect与useEffect都是在render后执行,并且先同步执行useLayoutEffect,后异步执行useEffect
执行顺序示例:
function ExampleComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('useEffect 执行'); // 后执行
});
useLayoutEffect(() => {
console.log('useLayoutEffect 执行'); // 先执行
});
return (
<div onClick={() => setCount(c => c + 1)}>
点击次数:{count}
</div>
);
}
四、 hooks之状态派生与保存[缓存数据]
4.1 useMemo:每次重新渲染的时候能够缓存计算的结果。
基础介绍:
const cachedValue = useMemo(calculateValue,dependencies)
-
① calculateValue:第一个参数为一个函数,函数的返回值作为缓存值,如果dependencies没有发生变化,React将直接返回相同值。否则,将会再次调用calculateValue并返回最新结果,然后缓存该结果以便下次重复使用
-
② dependencies: 第二个参数为一个
数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。 -
③ cachedValue:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。
示例:
import React, { useMemo, useState } from 'react';
export default function App() {
const [arr, setArr] = useState(new Array(10).fill(0));
const [expensiveValue, setExpensiveValue] = useState(null);
// 使用 useMemo 来记忆计算结果
const memoizedValue = useMemo(() => {
console.log('memoizedValue 被调起使用');
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
}, [arr.length]); // 仅当 arr.length 变化时重新计算
// 模拟一个昂贵的计算
const handleCalculate = () => {
setExpensiveValue(memoizedValue);
};
return (
<div>
<p>Array.length: {arr.length}</p>
<p>Value: {expensiveValue}</p>
<button onClick={() => setArr(prev => [...prev, 0])}>数组长度+1</button>
<button onClick={handleCalculate}>计算结果</button>
</div>
);
}
4.2 useCallback:是一个允许你在多次渲染中缓存函数的React Hook。
const cachedFn = useCallback(fn,dependencies)
使用场景
- 优化子组件渲染次数
4.3 React.memo:memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。
每次组件是否发生变化,与组件接收的参数props里的数据指向内存中映射地址有关,地址发生变化,组件会再次渲染,没有变化,则不渲染。
- 传递不变的基础类型值,组件不会再次渲染
- 传递会变化的基础类型值,组件会再次渲染
- 传递引用数据类型值,组件会再次渲染
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
### 参数
1:
如何使用:
在父组件外部添加:
const MemoizedChildComponent = memo(ChildComponent)
import React, { useState, useEffect, memo } from 'react';
// 定义子组件的类型接口
interface PropsType {
counter: number;
}
// 子组件
function ChildComponent({ counter }: PropsType) {
console.log('子组件被渲染');
return (
<div>
{counter}
</div>
);
}
// 使用 memo 包裹子组件
const MemoizedChildComponent = memo(ChildComponent,(oldProps,newProps)=>true);
// 父组件
export default () => {
const [counter, setCounter] = useState<number>(0);
// 处理计数器增加
const handleCounter = () => {
setCounter(prevState => prevState + 1);
};
console.log('父组件被渲染');
return (
<div>
<h1>父组件</h1>
<p>counter: {counter}</p>
<button onClick={handleCounter}>点击+1</button>
<MemoizedChildComponent counter={counter} />
</div>
);
};
4.4 React.memo、useMemo与useCallback的区别
-
React.memo是一个高阶组件,控制函数组件的重新渲染,将组件作为参数,函数返回值是一个新的组件
-
useMemo:是用来缓存计算结果,确保只有在依赖项发生变化时才会重新计算。
useMemo的实现方式是通过缓存计算结果,当依赖项发生变化时,重新计算结果并返回。【用来缓存DOM】 -
useCallback:是用于缓存函数,确保只有在依赖项发生变化时才会重新创建函数。
useCallback的实现方式是缓存函数本身,当依赖项发生变化时,重新创建函数并返回。【用来处理事件函数】
五、hooks之工具
5.1 useDebugValue
5.2 useId
六、 hooks之状态获取与传递
6.1 useRef
基础介绍:
useRef可以用来获取元素,缓存状态,接受一个状态initState作为初始值,返回一个ref对象cur,cur上有一个current属性就是ref对象需要获取的内容。
const cur = useRef(initState)
6.2 useContext
基础介绍:
使用useContext获取父组件传递的context值,这个当前值是最近的父级组件Provide设置的value的值,useContext参数一般是由createContext方式创建,
const contextValue = useContext(context)
useContext 接受一个参数,一般都是 context 对象,返回值为 context 对象内部保存的 value 值。
使用useContext时会出现的问题
- 1、
全局状态共享useContext允许在组件树中全局访问某个context值,当Context中的值发生变化时,所有使用该Context的组件都会重新渲染,即使组件实际上没有使用到发生变化的特定值。 - 2、
深层嵌套的组件树:在深层嵌套的组件树只能够使用useContext时,在顶层组件中没有频繁更新而底层组件频繁读取context值的情况下,可能顶层组件也会渲染。
为啥useContext会出现性能问题
Provider的value更新,所有useContext消费组件均重渲染,即使依赖数据未变。useContext订阅完整Context对象,React通过比较value的引用来判断变更。
解决上述问题
- 拆分多个Context
- 使用React.memo组件缓存
- 使用useMemo优化Context值
- 使用专门的状态管理库如Zustand等