1、useState
通过传入 useState 参数后返回一个带有默认状态和改变状态函数的数组。通过传入新状态给函数来改变原本的状态值。值得注意的是 useState 不帮助你处理状态,相较于 setState 非覆盖式更新状态,useState 覆盖式更新状态,需要开发者自己处理逻辑。
useState主要用于给组件添加状态变量。注意,我们只能在组件的顶层或自定义的 Hooks 中调用。
基础使用
js
const [age, setAge] = useState(42);
对象与数组的更新
js
//对象 setForm({ ...form, name: e.target.value // 更新这个属性 });
//数组 setTodos([ ...todos, { id: nextId++, title: title, done: false } ]);
函数的更新
js
//这是一个错误的做法
//这样的用法是把函数的返回值存储或更新到状态中,并不是把函数存储到状态中。
const [fn, setFn] = useState(someFunction);
function handleClick() { setFn(someOtherFunction); }
//想在状态中存储一个函数,你需要使用一个箭头函数来"包裹"它。这是正确的做法:
const [fn, setFn] = useState(() => someFunction);
function handleClick() { setFn(() => someOtherFunction)};
2、useEffect
有两个主要参数:第一个参数是一个函数,这个函数就是我们执行副作用操作的地方;第二个参数是一个依赖数组(dependency array),它告诉 React 什么时候应该执行这个副作用操作。
在类组件中,我们通常会使用 componentDidMount,componentDidUpdate 和 componentWillUnmount 生命周期方法来执行副作用操作,但是在函数组件中,我们使用 useEffect 来达到类似的效果。
在哪些场景中可以使用 useEffect 呢?
- 数据获取:如果你的组件需要从服务器获取数据,那么你可以使用 useEffect 来发起数据获取请求,并在请求完成后更新组件的状态。
- 订阅事件:如果你的组件需要监听一些事件(比如窗口的滚动事件、键盘事件等),那么你可以使用 useEffect 来订阅这些事件,并在事件触发时执行相应的逻辑。
- 副作用操作:如果你的组件需要在渲染时执行一些副作用操作(比如修改全局状态、设置定时器等),那么你可以使用 useEffect 来执行这些操作。
- 性能优化:在一些复杂的组件中,你可能需要根据组件的状态来调整渲染的逻辑。在这种情况下,你可以使用 useEffect 来根据状态的变化来执行不同的逻辑。
- 懒加载:如果你需要实现懒加载的功能(比如图片的延迟加载、文章的逐步展示等),那么你可以使用 useEffect 来监听相关的变化,并在变化发生时执行相应的逻辑。
基础使用
js
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() =>{
document.title = `You clicked ${count} times`;
},[count]);
// 只有当 count 发生变化时,才会执行 useEffect 中的函数
return (
<div>
<p>You clicked {count} times.</p>
<button onClick={() => setCount(count + 1)}>
Click me </button>
</div>
);
}
❤️在这个例子中,我们使用 useEffect 来更新文档的标题。每当点击按钮时,count 状态就会更新,从而触发 useEffect 中的函数,更新文档的标题。这就是 useEffect 的基本用法。
清除函数的作用
js
useEffect(() => {
const timer = setTimeout(() => {
console.log('这个消息将在 5 秒后显示'); }, 5000);
// 返回一个清理函数,用于清除定时器
return () => { clearTimeout(timer); };
}, []);
// 注意这里传入空的依赖数组,表示这个 useEffect 只会在组件首次渲染和卸载时运行
❤️在这个例子中通过返回一个函数,可以在useEffect钩子中注册一个清理函数,当组件卸载时,清理函数会自动被调用。这样,就可以确保在组件卸载时,任何需要被清理的资源或状态都会被正确地清除。
不同的依赖数组的区别
js
//1、如果第二个参数不写,那么它将在每次渲染后执行。
useEffect(() => {
console.log('这个函数将在每次渲染后执行');
});
//2、第二个参数是空数组,那么它只会在组件首次渲染时执行一次。
useEffect(() => {
console.log('这个函数只会在组件首次渲染时执行一次');
}, []);
//3、依赖数组中的值发生变化时执行。
const [count, setCount] = useState(0);
useEffect(() => {
console.log('这个函数将在 count 发生变化时执行');
}, [count]); // 这里的依赖数组包含 count 变量
3、useLayoutEffect(同步执行副作用)
赋值给 useEffect 的函数会在组件渲染到屏幕之后执行 useLayoutEffect ,它会在所有的 DOM 变更之后同步调用。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新
基础使用
js
import React, { useState, useEffect, useLayoutEffect } from "react";
function App() {
const [width, setWidth] = useState(0);
useEffect(() => {
console.log("useEffect");
console.log(width)
});
useLayoutEffect(() => {
const title = document.querySelector("#title");
const titleWidth = title.getBoundingClientRect().width;
console.log("useLayoutEffect");
if (width !== titleWidth) {
setWidth(titleWidth);
}
});
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
);
}
export default App
❤️在以上例子中的总结如下:
- 首次渲染,Dom构建完成后执行useLayoutEffect,判断宽度不相同,去派发;
- 更改状态,在组件首次渲染完成后执行useEffect,因为width更改,又会render;
- 再次在dom完成后执行useLayoutEffect,发现相等,不在派发,随后渲染完成后执行useEffect。
useEffect和useLayoutEffect的区别
- 执行时机:useEffect是异步执行的,其执行时机是在浏览器完成渲染之后(DOM更新后执行的)。而useLayoutEffect是同步执行的,其执行时机是在浏览器把内容真正渲染到界面之前(浏览器绘制之前执行),和componentDidMount等价。
- 同步/异步:useEffect是异步的,这意味着它会在当前的浏览器帧结束之后执行。而useLayoutEffect是同步的,它会在浏览器开始渲染之前执行,因此可以用来同步获取布局(ref)信息。
useEffect和useLayoutEffect在react中是怎么执行的?
- react 在 diff 后,会进入到 commit 阶段,准备把虚拟 DOM 发生的变化映射到真实 DOM 上,在 commit 阶段的前期,会把使用了 useEffect 组件产生的生命周期函数入列到 React 自己维护的调度队列中,给予一个普通的优先级,让这些生命周期函数异步执行;
- 随后内存中的真实 DOM 已经变化,浏览器也没有立刻渲染到屏幕上。此时会进行收尾工作,同步执行对应的生命周期方法,我们说的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函数都是在这个阶段被同步执行;
- 浏览器把发生变化的 DOM 渲染到屏幕上,到此为止 react 仅用一次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成;
- 浏览器渲染完成后,浏览器通知 react 自己处于空闲阶段,react 开始执行自己调度队列中的任务,此时才开始执行 useEffect(create, deps) 的产生的函数。
为什么建议将修改DOM的操作放在 useLayoutEffect 中,而不是 useEffect ?
❤️当DOM被修改时,浏览器的线程处于被阻塞阶段,此时还没有发生回流、重绘。由于内存中的DOM已经被修改,通过useLayoutEffect可以拿到最新的DOM节点,并且在此时对DOM进行样式上的修改。假设修改了元素的 height,这些修改会一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。相反,如果将修改DOM操作放在useEffect中,会导致浏览器的重绘和回流。因此,如果涉及修改DOM的操作,建议将其放入useLayoutEffect中,以避免不必要的重绘和回流。
4、useCallback
useCallback 用于缓存需要在组件中多次调用的回调函数,接受两个参数:回调函数和一个依赖项数组。只有当依赖项发生变化时,才会重新创建这个回调函数。这有助于减少不必要的重新渲染,尤其是在子组件中使用回调函数时。
useCallback执行过程
- 组件渲染时:React 将会执行函数组件的整个函数体, useCallback 也会调用。
- 缓存函数引用: 当 useCallback 被调用时,React 将会缓存传入的函数,并且返回缓存函数。不会在每次渲染时重新创建。
- 依赖项检测:接受一个依赖项数组。React 会监视这个依赖项数组,并且在数组中的任何一个值发生变化时,重新创建函数。如果依赖项数组中的值保持不变,React 将会跳过函数的重新创建。
- 传递给子组件:缓存的函数可以作为 props 传递给子组件 ,父组件重新渲染,子组件也不会重新渲染。
基础使用
js
const memoizedCallback = useCallback(
() => {
// 函数体
},
[dependency1, dependency2, ...] // 依赖数组
);
useCallback配合React.memo使用
js
import react, {memo, useCallback, useState} from "react";
function ParentComponent(){
const [value,setValue] = useState(0);
const changeChildren = useCallback(()=>{
console.log("传入子组件") },[])
return (
<div>
<span> Value:{value}
<button onClick={()=>{ setValue(value+1) }}>
click Btn
</button>
</span>
<ChildrenComponent fun={changeChildren}/>
</div>
)
}
const ChildrenComponent = memo(()=>{
console.log("children");
return <div>children</div>
})
export default ParentComponent;
❤️在以上例子中的总结如下:
- React.memo检测的是props中数据的栈地址是否改变。而父组件重新构建的时候,会重新构建父组件中的所有函数,新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。
- 使用useCallBack后父组件重新渲染,子组件中的函数就会因为被useCallBack保护而返回旧的函数地址,子组件就不会检测成地址变化,也就不会重选渲染。
注意事项
- 避免滥用: 尽量避免过度使用 useCallback。如果所有函数都是用 useCallback,React内部将所有函数放在一个缓存数组中,如果依赖项中任一参数发生变化都会重新创建此函数;
- 性能问题:缓存大量函数,这些函数占用了大量内存或者使用了昂贵的计算,可能会对性能造成负面影响,还可能会降低代码的可读性和可维护性;
- 简单组件:对于没有经过 React.memo 优化的子组件或者那些不会因为 prop 变化而重新渲染的组件,使用useCallback 是不必要的。
- 不涉及其它 Hooks 的函数:如果一个函数并不被用作其他 Hooks 的依赖,并且也不被传递给任何子组件,那么没有理由使用useCallback。
- 不要将非必要参数传入到依赖项中:非必要的参数更新将会重新创建函数。
5、useMemo
核心目的是通过缓存计算结果,避免在组件渲染时进行不必要的重复计算,从而优化性能。这意味着只有当其依赖项发生变化时 useMemo 才会重新计算这个值,否则它将重用之前的结果。
基础使用
js
const memoizedValue = useMemo(() => expensiveCalculation(), [/* 依赖项 */]);
❤️在以上例子中的总结如下:
- expensiveCalculation 是一个昂贵的计算函数;
- 使用 useMemo 缓存了它的计算结果,并且只有当依赖项数组中的值发生变化时才会重新计算。
useMemo的执行过程
js
import react, {useEffect, useMemo, useState} from "react";
import axios from "axios";
export default function Cinema() {
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
const [inputVal, setInputVal] = useState('');
const handleGetList = () => {
setLoading(true); axios({
url: "https://m.maizuo.com/gatewaycityId=440300&pageNum=1&pageSize=15&type=1&k=8857390",
method: 'get',
headers: { "X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"17110951061639191448387585"}', "X-Host": "mall.film-ticket.film.list" }
})
.then(res => {
if (res && res.data.status === 0) {
setList(res.data.data.films)
} else {
setList([])
} setLoading(false) })
}
const getList = useMemo(() => {
return list.filter(item => item.name.includes(inputVal));
}, [list, inputVal]);
useEffect(() => {
handleGetList();
}, [])
return (
<div>
<input value={inputVal} onChange={(e) => setInputVal(e.target.value)}/>
<ul> {!loading && getList.map(item => {
return <li key={item.filmId}>{item.name}</li> })
} </ul> {loading && <span>Loading.........</span>} </div>
)
}
❤️在以上例子中的总结如下:
- 初始化阶段:useMemo 的回调函数在组件渲染时被执行,计算初始的 memoized 值。
- 依赖项数组变化时:当 list 或 inputVal 发生变化时,useMemo 的回调函数会被重新执行,计算新的 memoized 值。
- 依赖项数组不变时:如果 list 和 inputVal 的值保持不变,useMemo 会返回之前存储的 memoized 值,避免重复计算。
useMemo和useCallback的区别及使用场景
* * *
注意事项
- 不要滥用:只在有昂贵的计算或大型引用类型传递给子组件时考虑使用,过度使用可能导致复杂性和性能问题;
- 正确选择依赖项:确保依赖数组包含所有在计算函数中使用的变量,否则可能导致缓存的值不会在依赖变化时重新计算;
- 深层比较:注意引用类型的依赖项是否需要深层比较,以确保正确触发重新计算;
- 避免内存占用: 小心缓存大型对象或数组,以避免不必要的内存占用。在某些情况下,直接传递引用类型的变化可能更合适;
- 返回值:useMemo 返回计算的值,而不是函数。如果需要缓存函数,应该使用 useCallback;
- 性能监控工具:使用性能监控工具(如 React DevTools)检查 useMemo 缓存的值是否按预期工作,有助于排除性能问题和调试;
- 组件层次结构:useMemo 只在当前组件内有效,如果需要在多个组件之间共享缓存值,可能需要提升状态或使用上下文(Context)。
6、useRef
主要用于在函数组件中存储持久性的值,并且不会触发组件的重新渲染。
基础使用
js
const ref = useRef(initialValue);
❤️注意:
- 返回一个可变的 ref 对象,该对象只有个 .current 属性,初始值为传入的参数( initialValue );
- 返回的 ref 对象在组件的整个生命周期内保持不变;
- 不要在渲染期间写入或者读取 ref.current;
- 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方。
使用场景
- 获取 DOM 元素:
js
function TextInput() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</div>
);
}
- 存储任意可变值(不能使用在页面上)
js
import React, { useRef, useState } from 'react';
function MyComponent() {
const counterRef = useRef(0);
// 存储计数器值
const increment = () => {
counterRef.current += 1;// 修改计数器值,不会触发重新渲染
console.log('Counter:', counterRef.current);
};
return (
<div>
<button onClick={increment}>Increment</button>
</div>
);
}
- 在 effect 中保存变量的前一个值(保存props 或 state 的上一次值,可以使用useRef结合useEffect)
js
import React, { useRef, useEffect } from 'react';
function MyComponent({ value }) {
const prevValueRef = useRef(); // 保存前一个值的引用
useEffect(() => {
prevValueRef.current = value; // 在每次渲染结束后更新 prevValueRef 的值
});
const prevValue = prevValueRef.current; // 获取前一个值
return (
<div>
<p>Current value: {value}</p>
<p>Previous value: {prevValue}</p>
</div>
);
}
注意事项
❤️避免重复创建Ref(用null作为初始值,渲染的过程判断仅在null时去计算或调用有副作用的方法)
js
function ClickCounter() {
// good
const countRef = useRef(null);
// good
if (countRef.current === null) {
countRef.current = getInitialCount();
}
function handleClick() {
countRef.current += 1;
console.log(`Button clicked ${countRef.current} times.`);
}
return <button onClick={handleClick}>Click me!</button>;
}
7、useContext
用于在函数式组件中访问上下文(Context)的值。跨层级组件之间共享,传递数据的机制。
基础使用
- 创建 Context
js
const MyContext = React.createContext(defaultValue);
❤️这里的 defaultValue 是当组件不在任何 Context Provider 内部时的默认值,defaultValue可以用 null,但 React 官方建议提供一个有意义的默认值,这样可以让调用usecontext组件更安全。
- 使用 Context Provider
js
<MyContext.Provider value={someValue}>
{/* 子组件 */}
</MyContext.Provider>
❤️为了在组件树中使用这个 context ,需要使用 组件,接受一个valueprop,这就是在其他子组件中共享的值。
- 在组件中访问 Context
js
function MyComponent() {
const contextValue = useContext(MyContext);
return <div>{contextValue}</div>;
}
❤️在函数组件中,可以使用 useContext 来访问这个 context 的值。这里的 contextValue 就是第二步传入的 someValue,而且
contextValue 获取到的永远是最新的值。
使用案例
js
import React, { useContext } from 'react';
// 1. 创建 Context
const ThemeContext = React.createContext('light');
function App() {
return (
// 2. 使用 Context Provider
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemeButton />
</div>
);
}
function ThemeButton() {
// 3. 在组件中访问 Context
const theme = useContext(ThemeContext);
return <button>{theme} theme</button>;
}
export default App;
❤️在以上例子中的总结如下:
- App 中引用了 ThemeContext 并传了值;
- ThemeButton 是 App 的孙组件,这二者之间没有通过 Toolbar 进行嵌套传值;
- ThemeButton 依然通过useContext拿到了 App 里的值。
- React.createContext和useContext共同组成了一个管道,通过这个管道,可以进行跨组件共享状态。
覆盖Provider value
js
<ThemeContext.Provider value="dark">
...
<ThemeContext.Provider value="light">
<Footer />
</ThemeContext.Provider>
...
</ThemeContext.Provider>
//当我们调用多个相同 Context,会实现value的覆盖
注意事项
当 Provider 的 value 属性值发生变化时,所有使用了 useContext 的组件都将重新渲染。如果value经常变化,或者消费者组件很多,那么这会引起大量的不必要的渲染。
- 细化 Context:如果 context 包含许多不同的状态值,可以将它们分解成多个 context。
- 使用 useMemo 和 useCallback 优化 value:为了避免 value 变化造成子孙组件频繁的重新渲染,可以使用 useMemo 和useCallback 对参数和方法进行缓存,减少value的无意义更新。
8、useReducer
可以将其看作是一种"全局变量"的替代方案。使用 useReducer 可以更好地组织和管理组件的状态,并且适用于一些具有复杂状态逻辑的组件。与useState相比,useReducer更适用于状态逻辑比较复杂,或者需要多个状态之间存在关联的情况。使用useReducer时,可以将所有的状态更新逻辑集中在reducer函数中,使代码更加清晰和易于维护。
使用案例
- 创建reducer函数,接受两个参数:当前的状态(state)和要执行的action;reducer 函数根据 action的类型来更新状态,并返回新的状态
js
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default: return state;
}
};
- 调用useReducer 将返回当前状态和 dispatch 函数,dispatch函数用于发送action以触发状态更新。
js
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
}
注意事项
- 纯函数reducer:确保reducer函数是纯函数,接收状态和action,返回新状态,避免副作用。
- 合理划分状态:将状态合理地划分为多个reducer,避免将所有状态放在一个reducer中。
- 适度使用:只在需要复杂状态逻辑时使用useReducer,简单状态管理优先考虑useState。
- 初始状态选择:提供合适的初始状态,确保与组件预期行为相符,并具有合理的默认值。
- 避免直接修改状态:始终通过dispatch函数发送action来更新状态,避免直接修改状态对象。
- 性能优化:使用useCallback和useMemo优化性能,减少不必要的渲染和计算。
- 充分利用Context:useReducer通常与Context一起使用,便于状态共享和管理。