文章目录
- 前言
- 1、React的组件创建方式
- 2、什么是Hook?
- 3、Hook总的使用规则
- 一、useState
- 二、useRef
- 三、useEffect
- 四、useLayoutEffect
- 五、useReducer
- 六、useContext
- 七、memo与useMemo、useCallback
- 1、memo
- 2、useMemo
- 3、useCallback
- 4、三者区别
- 八、useImperativeHandle
前言
Hooks是 React 16.8 版本引入的一项特性,它允许在函数式组件中使用状态和其他 React 特性,而不需要使用类组件。
1、React的组件创建方式
1)类式组件
能够对状态的管理与切换进行操控,但是在简单的页面中使用类式组件会使代码显得很重,并且状态逻辑难以复用。
虽然可以使用 render props (渲染属性)和高阶组件进行优化,但这两种方式都会将原来的组件用一个容器包裹起来,形成层级嵌套,代码冗余。而且在类式组件中,this的指向经常容易引起问题。
2)函数式组件
没有了类式组件中的状态,没有生命周期,更不存在this,转而代之的则是一系列的hook。
2、什么是Hook?
Hook就是钩子,作用是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。
React中文官网:https://react.docschina.org/reference/react/hooks
3、Hook总的使用规则
-
只能在函数最外层调用 hook,即:不要在循环、条件判断或者子函数中调用。这是为了确保 Hook在每一次渲染中都按照同样的顺序被调用。
-
只能在React函数组件或自定义 hook中使用。这是为了确保组件的状态逻辑在代码中清晰可见。
PS: 本文React的版本:
一、useState
1、作用: 允许我们在函数组件内部声明并管理状态(state),即useState 相当于我们在原生 JS 中定义变量。
2、语法:const [initialState, setInitialState] = useState(initialState);
-
useState(initialState)有一个初始化值作为参数,initialState可以是任意值。
-
返回包含两个值数组,第一个是值,第二个是set方法。
3、注意事项:
-
useState只能用于函数组件,可以有多个;类组件是用setState,且一个类只能有一个setState。
-
useState不能在循环、条件或嵌套函数中调用。
-
当useState里面的状态为一个对象时,useState是不会进行局部更新的,而是将修改后的内容将之前的内容进行覆盖。
-
useState 是一个异步操作,它并不会立即更新状态。
也就是说,在函数组件中调用 useState 更新状态时,React 会将新的状态安排进下一次渲染,并在之后的某个时候才会执行更新,这就导致在更新状态后立即访问该状态可能会得到之前的值,而不是最新的值。
【比如在下面示例中setCount后,直接获取count,count的值是不变的】
5、解决数据更新办法:先使用useRef进行存储数据,再使用useEffect监听数据变化,并进行更新。
import { useState, useRef, useEffect } from 'react';
const App = () => {
// 初始化状态和对应的更新函数
const [count, setCount] = useState(1);
const countRef = useRef();
// 当需要更新stateValue时,setCount可以直接传递新的状态值
const handleUpdate = (newCount: number) => {
setCount(newCount);
};
// 监听count值实时更新
useEffect(() => {
countRef.current = info;
}, [count]);
// 渲染count值
return <div>{count}</div>
}
6、自定义模拟源码实现useState方法
// 每次更新时,函数组件会被重新调用,为了新值被记录而不是一直被重新赋上initialState,需要将其提到外部作用域声明
// _state定义为数组是为了解决多次调用的问题,react为了保证useState的执行顺序,还规定了不能在循环、条件或嵌套函数中调用
let _state = [], _index = 0;
function useState(initialState) {
let curIndex = _index; // 记录当前操作的索引
if (typeof initialState === "function") {
initialState = initialState();
}
// 用hasOwnProperty判断当前索引是否被创建,如果有,说明已经对这个state赋初始值了,如果没有,则说明是新创建的,需要使用initialState赋初始值。
_state[curIndex] = !_state.hasOwnProperty(curIndex) ? initialState : _state[curIndex];
// 定义回调函数
const setState = newState => {
if (typeof newState === "function") {
newState = newState(_state[curIndex]);
}
// 使用Object.is来比较_state[curIndex]是否变化,若无,则跳过更新
if (Object.is(_state[curIndex], newState)) return;
_state[curIndex] = newState;
// 简单的更新根视图
ReactDOM.render(<App />, rootElement);
_index = 0; // 每更新一次都需要将_index归零,才不会不断重复增加_state
};
_index += 1; // 下一个操作的索引
return [_state[curIndex], setState];
}
对比react源码中:_state
其实对应 React 的 memoizedState
,而 _index
实际上是利用了链表。
二、useRef
1、作用:用于获取真的DOM节点,返回一个对象,对象里面有current属性存放着获取到的原生DOM对象。
2、语法:const inputRef = useRef(null);
3、使用示例:
import React, { useRef } from 'react'
function App() {
const inputRef = useRef(null)
const handleClick = () => {
console.log(inputRef.current)
}
return (
<div className="App">
<input ref={inputRef} type="text" />
<button onClick={handleClick}>获取DOM节点</button>
</div>
)
}
export default App
4、注意事项:
- useRef虽然可以获取到DOM对象,但是建议轻易不要这么做,如果必须要获取,也尽量是读取而不要修改,如果必需要修改也要尽量减少修改的次数,总之能不用就不用。
- 自己创建的对象组件每次渲染时都会重新创建一个新的对象,而通过
useRef()
创建的对象可以确保组件每次的重渲染获取到的都是相同的对象。
三、useEffect
1、作用:可以检测数据的更新,主要用于处理副作用操作,比如:订阅数据、手动更改 DOM、设置定时器、添加事件监听器等,并且可以指定在什么条件下进行清理。
2、副作用
-
含义:指一段和当前执行结果无关的代码,比如说要修改函数外部的某个变量,要发起一个请求有两个参数。
-
副作用操作:数据获取,设置订阅以及手动更改 React 组件中的 DOM。
3、语法:useEffect(() => {}, [param]);
参数说明:
-
第一个参数为要进行的异步操作;
-
第二个参数为一个数组,里面为要检测的依赖项,只要数组里面的依赖项发生了变化,useEffect就会执行。
当第二个参数不传时,在每次组件渲染时useEffect都会执行,类似于componentDidUpdate
生命周期钩子。第一个参数返回的函数就相当于componentWillUnmont
。
4、特别说明:
- 默认情况下,React 会在每次渲染后调用useEffect,包括第一次渲染:DOM渲染》调用副作用函数》页面展示最新数据信息。
- useEffect 可以通过返回一个函数来指定如何清除相关的副作用操作,便于将添加和移除订阅的逻辑放在一起。
- 重新渲染:默认都会生成新的effect,替换掉之前的。
- 清除阶段:在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。
5、使用示例:
import { useEffect } from 'react';
function App() {
// 1、无依赖项,会一值执行
useEffect(() => {
// 操作
});
// 2、依赖项为空,组件初始化会执行一次
useEffect(() => {
// 操作
},[]);
// 3、依赖于某个变量或函数,在依赖项发生变化后触发
useEffect(() => {
// 在组件渲染后执行副作用操作
// 返回一个清理函数(可选)
return () => {
// 在组件卸载或重新渲染之前执行清理操作, 该清理函数将在组件卸载或重新渲染之前执行
};
}, [/* 依赖项数组 */]);
return (<div></div>);
}
6、注意事项:
-
数据获取和订阅:可以在
useEffect
中进行异步操作,如:发起网络请求获取数据或订阅事件,但要确保在清理函数中取消订阅或中断请求,以避免内存泄漏。 -
清理函数的使用:如果副作用函数需要进行清理操作,如:取消订阅或清除定时器,要在副作用函数中返回一个清理函数,该清理函数将在组件卸载或重新渲染之前执行。
-
异步操作和更新状态:在副作用函数中进行异步操作时,要确保正确处理状态的更新。可以使用函数式更新或通过依赖项数组传入更新的状态。
-
在开发的时候发现,当
useEffect
第二个参数为空数组时,明明是在组件挂载之后执行一次,但是会发现在里面打印内容时,会有两次输出。官方文档给出了相应的解释,这种情况只在开发环境中才如此,是 react 方便使用者在开发环境调试。
对应的解决办法: 根据实际业务场景去解决,有清除函数时要加上,它的依赖项也很重要,有些没有必要监听的依赖,要从Effect中独立出来,变为非响应式的值。
四、useLayoutEffect
1、作用:与 useEffect
类似,但它是在 DOM 变更之后同步执行,不是在浏览器绘制之后执行。它会在浏览器布局和绘制之前同步执行回调函数。
2、使用场景:使用 useLayoutEffect
可以在浏览器布局完成后立即执行一些操作,以确保获取到最新的 DOM 布局信息,并在下一次渲染之前同步地更新 UI。如:准确地测量 DOM 元素的尺寸、位置或进行 DOM 操作。
3、示例:
import React, { useRef, useLayoutEffect } from 'react';
function MeasureElement() {
const ref = useRef();
useLayoutEffect(() => {
const element = ref.current; // 在浏览器布局完成后立即执行操作
const { width, height } = element.getBoundingClientRect(); // 获取被测量元素的宽度和高度
console.log('Element size:', width, height); // 使用测量结果进行操作
}, []);
return <div ref={ref}>Measure me!</div>;
}
function App() {
return (
<div>
<MeasureElement />
</div>
);
}
4、注意事项:
由于 useLayoutEffect
在浏览器布局之后同步执行,因此它的执行会阻塞浏览器的渲染过程。所以,一般只有在需要同步更新 UI 或测量 DOM 元素时才使用 useLayoutEffect
,避免造成性能问题。
五、useReducer
1、作用:通常用于管理具有复杂状态和行为的组件,尤其是涉及到多个状态转换的情况。类似于 Redux 中的 reducer,接收一个状态和操作函数,并返回新的状态和派发操作的函数。
2、语法:const [state, dispatch] = useReducer(reducer, initialState);
参数说明:
- 第一个是包含状态转换逻辑的函数(reducer);
- 第二个是初始状态。
返回值:一个包含当前状态和 dispatch 函数的数组。
3、使用示例
先定义一个reducer函数:
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
然后使用reducer:
import React, { useReducer } from 'react';
// 设置初始值
const initialState = { count: 0 };
function Counter() {
// 定义状态state和派发操作的函数dispatch
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
// 根据操作的类型通过调用dispatch派发操作,并由reducer函数来更新状态
<button onClick={() => dispatch({ type: 'INCREMENT' })}>增加</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>减少</button>
</div>
);
}
4、常用场景
1)计数器:可以使用 useReducer
来管理计数器的状态和操作,如:增加、减少计数等。
2)表单处理:使用 useReducer
可以更好地管理表单的状态和用户输入的操作,以及进行表单验证等复杂逻辑。
3)数据列表:当处理复杂的数据列表时,使用 useReducer
可以更好地管理数据的加载、筛选、排序等操作,以及处理分页等功能。
4、总结
useReducer
适用于需要管理复杂状态逻辑的组件,并且可以帮助组织和更新状态,以及处理相关的操作。甚至可以代替使用 useState
的方式,特别适合管理具有多个操作类型和相关状态的组件。
六、useContext
1、作用:用于在组件树中传递数据,通过组件之间的数据状态共享,避免了逐级传递props的缺陷。
2、使用说明:通过 createContext
来创建 context
对象,然后使用 context
对象上的 Procider
来提供数据,在后代组件中通过 useContext
来接收数据。
3、示例
import React, { createContext } from 'react';
const MyContext = createContext(); // 创建上下文对象
function MyContextProvider({ children }) {
const sharedData = 'Shared Data';
return (
// 通过 MyContext.Provider提供器将共享数据sharedData传递给子组件
<MyContext.Provider value={sharedData}>
{children}
</MyContext.Provider>
);
}
在需要访问上下文数据的组件中使用 useContext
:
import React, { useContext } from 'react';
function App() {
const sharedData = useContext(MyContext);
return (
<div> Shared Data: {sharedData} </div>
);
}
4、注意事项
-
使用
useContext
前,要确保已在组件树中的某个地方提供了上下文。 -
数据更新问题:当上下文数据发生变化时,使用
useContext
的组件会自动重新渲染。即:当共享数据发生更改时,使用该上下文的所有组件都会更新。 -
嵌套问题:React 允许上下文进行嵌套。在嵌套的情况下,使用
useContext
会获取最接近的上层提供器的值。 -
性能问题:
useContext
的默认行为是每次组件渲染时都重新计算上下文的值。这可能会导致性能问题,尤其是当上下文的值频繁变化时。为了避免不必要的重新渲染,可以使用memo
结合useContext
进行优化。 -
代码耦合问题:上下文是用于共享数据的有用工具,过度使用可能导致组件之间的耦合性增加。建议在需要共享数据的组件之间仔细考虑使用上下文的合适程度。
七、memo与useMemo、useCallback
在 React 中,每当函数组件重新渲染时,函数组件的所有局部变量都会重新初始化。
如果将一个新的函数作为 prop 传递给子组件,即使这个函数具有相同的逻辑,由于它是一个新的引用,子组件可能会被重新渲染,从而可能引起性能问题。
在使用useMemo、useCallback
时经常会遇到被缓存的值/函数并不是最新值,换句话说就是,useMemo、useCallback
中的值并不是最新值。
因为带有缓存的作用,所以对应的内存占用会随着使用数量的增加而增加。
1、memo
memo
是一个用于优化性能的 React 高阶组件。
语法: const Test = memo(C, cb?)
参数说明:
-
第一个参数是要被记忆的组件(
memo
并不会对此组件进行任何更改)。C
组件就是要被记忆的组件,而memo
函数则可以理解成它是C
组件的shouldComponentUpdate
生命周期方法。 -
第二个参数
cb
则是渲染C
组件时所调用的回调函数,这个函数会收到当前组件的新旧state以决定本次是否重新渲染。
2、useMemo
1)、作用:用于在组件渲染过程中进行性能优化,避免不必要的计算和重复渲染。类似于useCallback但其主要用于缓存计算结果,而不是缓存回调函数。
2)、语法:useMemo(fn, dependencies);
3)、使用示例
import React, { useMemo, useState } from 'react';
function ExpensiveComponent() {
// 假设calcExpensiveValue是一个计算非常耗时且复杂的函数
function calcExpensiveValue() {
// ...包含一些复杂的计算逻辑
return Math.random();
}
// 使用useMemo缓存计算结果,将空数组作为依赖项传递给useMemo,确保只在组件首次渲染时执行一次计算,避免了每次渲染时都重新计算
const expensiveValue = useMemo(() => calcExpensiveValue(), []);
return (
<div> Expensive Value: {expensiveValue} </div>
);
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ExpensiveComponent />
</div>
);
}
4)、特别说明:
- 若没有提供依赖项,
useMemo
在每次渲染时都会计算新的值,若提供依赖项数组,当某个依赖项改变时才会重新计算。 - 不要在fn函数内部执行与渲染无关的操作,如:副操作等,适合使用在其它hook中的逻辑。
5)、常见使用场景:
-
缓存计算结果:当需要根据某些输入计算结果时,可以使用
useMemo
缓存计算结果,避免重复计算,特别适合计算量大的场景。 -
避免不必要的重复渲染:当一个组件依赖于某个状态或属性,但这些状态或属性的变化并不会影响到组件的渲染结果,可以使用
useMemo
来缓存渲染结果,避免不必要的重复渲染。 -
优化子组件的渲染:当将一个计算结果作为属性传递给子组件时,可以使用
useMemo
缓存计算结果,以避免在每次父组件渲染时都重新计算并传递给子组件。 -
性能敏感的比较操作:当需要进行一些性能敏感的比较操作,如:深度比较对象或数组时,可以使用
useMemo
缓存比较结果,以避免在每次渲染时重新执行比较操作。
3、useCallback
1)作用:主要目的是性能优化用于缓存回调函数,以便在依赖项变化时避免不必要的函数重新创建。
2)语法:useCallback(fn, dependencies);
参数说明:
- 第一个参数fn是要缓存的函数,
useCallback
会在其依赖项发生变化时调用此函数。 - 第二个参数
dependencies
则是依赖项列表,只有依赖发生变化时,才会重新调用所传入的回调函数。
3)注意:在使用useCallback
时可能会造成由于闭包问题而无法在useCallback
中获取新值的问题。
// 自定义hooks:解决使用useCallback时造成的闭包问题而拿不到新值
const useEventCallback = <T extends (...args: any[]) => any>(fn: T) => {
const ref = useRef(fn)
useLayoutEffect(() => {
ref.current = fn
})
return useCallback(((...args) => ref.current.call(window, ...args)) as T, [])
}
4、三者区别
-
memo
用于避免在父组件重新渲染时重新渲染子组件,只有在属性发生变化时重新渲染组件。 -
useMemo
用于避免在组件重新渲染时执行昂贵的计算,只有在依赖发生变化时重新计算值。 -
useCallback
用于避免在组件重新渲染时创建新的函数实例,只有在依赖发生变化时返回新的函数实例。
八、useImperativeHandle
1、作用:允许我们将特定方法向下传递到子组件。简单来说,就是可以让我们控制父组件直接调用子组件中暴露出来的方法或属性。
但其需要与 forwardRef
一起使用,forwardRef
可以将父组件传递给子组件。
2、语法:useImperativeHandle(ref, cb);
参数说明:两个参数,一个 ref
和一个提供给该 ref 的回调函数
,在回调函数中定义子组件向父组件暴露出的方法或属性,并返回一个值或函数,该值或函数可以从 ref
中访问。
3、示例:
import React, { forwardRef, useImperativeHandle } from 'react';
// 子组件
const Child = forwardRef((props, ref) => {
// 创建一个childRef对象,以便可以在回调函数中使用
const childRef = useRef();
// 定义focusChild方法和someValue属性,并将其返回给父组件
useImperativeHandle(ref, () => ({
focusChild: () => {
childRef.current.focus();
},
someValue: '子组件暴露出来的属性',
}));
return <input type="text" ref={childRef} />;
});
const Parent = () => {
const childRef = useRef();
const handleClick = () => {
childRef.current.focusChild && childRef.current.focusChild();
};
return (
<div>
<Child ref={childRef} />
<button onClick={handleClick}>点击获取焦点</button>
</div>
);
};
export default Parent;
学无止境!关于React的Hooks就暂时介绍这么多啦,更多的建议看看 官网 哦~