React 中的 Hook 概览
前言
在学习 react 的过程中我们常常会看到 Hook 这个词,那它到底是个什么呢,不就是个钩子吗有什么了不起的?
它在 react 中又能起到什么作用呢?
为什么能让面试官对它那么念念不忘,每次面试总是少不了它的身影。
今天就来看看 React 中几个常见的 Hook 吧 !!!
什么是Hook?
Hook 是一种在软件开发中常用的编程技术,也称之为 钩子编程,钩子(Hook)源于Windows的消息处理机制,通过设置钩子,应用程序对所有消息事件进行拦截,然后执行钩子函数。
通过拦截软件模块间的函数调用、消息传递、事件传递向应用程序中插入自定义代码以改变应用程序的行为,能够修改或扩展操作系统、应用程序或其他软件组件的行为的技术。
常见的Hook
Hook 可以应用于多种编程语言和操作系统。我们最常见的钩子就是之前讲到过的生命周期钩子函数。
一些常见的 hook :
- 钩子函数(Hook Function):在函数执行前或执行后执行自定义代码,常用于日志记录、性能监控等。
- 消息钩子(Message Hook):在操作系统接收到消息时执行自定义代码,常用于拦截鼠标、键盘等输入事件,或者在窗口关闭前进行一些操作。
- 系统钩子(System Hook):在操作系统级别拦截系统事件并执行自定义代码,常用于监控系统级别的事件,如进程创建、窗口消息等。
- Web Hook:在 Web 应用程序中,Web Hook 用于在特定事件发生时触发自定义代码,如提交表单、发布文章等。
react 中的 Hook
而在 react 官方文档中是这么介绍 Hook 的:
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook 的出现解决了 React 中的状态共享及组件生命周期管理混乱的问题。意味着 React 不会再存在无状态组件的情况。
使用 Hook 的优势:
- 提供涉及state 和生命周期钩子逻辑的抽象和复用,解决了过去因为和组件耦合导致的state和钩子的复用问题
- 将props的链式数据传导转化为横切式的传导方式,减少了props的传导层级,简化了结构
- 使函数组件也能操作state,完善了函数式组件的功能,避免了因为逻辑的复杂化时要把函数组件改成class组件的麻烦
- 和render-props/高阶组件实现方案的对比:比起别扭的render-props来,Hook的代码结构清晰明了。而相比起高阶组件,hook有内置的API,使用起来更简单方便
Hook的使用要求:
- Hook 是 React 16.8 的新增特性
- Hook只能在函数式组件或者自定义hook中使用,不能在类式组件中使用
- Hook可以作为子组件和其他class组件一起嵌套在组件层级中
- Hook只能在顶层调用,不能在循环、条件语句或嵌套函数中使用,否则可能会导致预料之外的 bug
react 中常见的Hook
React 中也有不少的Hook 它就是一种函数,可以让你在函数组件中使用 React 的一些特性,例如状态管理和生命周期方法。它们可以帮助你更轻松地编写可重用的代码,避免使用类组件时可能出现的一些问题。
React 中的 Hook:
- useState Hook
- Effect Hook
- useContext
- useReducer
- useCallback
- useMemo
- useRef
useState Hook
useState 让函数组件具有修改state的 能力,因为在函数式组件中没有 this,React 会在重复渲染时保留这个 state,而 useState 可以让你在函数组件中管理状态。
传参:useState(xxx)
方法里面唯一的参数就是初始 state, 将传入的值将作为属性的初始值,state 变量可以很好地存储对象和数组
返回值为:当前 state 以及更新 state 的函数。
import React, { useState } from 'react';
function Example() {
// 声明一个叫 count 的 state 变量 useState(0) 传参 0 为设置的初始值
const [count, setCount] = useState(0); //[] 数组解构 取到数组中对应位置的值 赋给相应变量
// 声明多个 state 变量,给不同的 state 变量取不同的名称 state 变量可以很好地存储对象和数组
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
return (
<div>
<!-- 在函数中,我们可以直接用 count -->
<p>You clicked {count} times</p>
<!-- 更新state setCount 和 count 变量,所以我们不需要 this -->
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
声明了一个 count 的 state 变量并赋值为 0。 React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。 我们可以通过调用 setCount 来更新当前的 count。
在 state 中存储两个不同的变量,只需设置不同的 useState
, 并且通过 useState 获取到的数据之间相互隔离。
Effect Hook
在React组件中,执行数据获取、订阅或手动修改DOM等操作被称为“副作用”,或者简称为“作用”。
useEffect
就是一个 Effect Hook,给函数组件增加了操作副作用的能力。在 render 之后执行,当你调用 useEffect
时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。
Hook 就是 js 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不能在其他函数中调用。
useEffect 的使用
useEffect 的功能更加强大,是所有函数的合体!同时具备了 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期函数的执行时机。
useEffect
还允许我们在组件内部直接访问 state 变量或 props ,因此可以在 useEffect 执行函数值的更新操作,这些操作可能会影响组件的状态或渲染结果。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(
() => {
// 在函数体内部执行的操作 等价于 componentDidMount & componentDidUpdate
// 使用浏览器的 API 更新页面标题
document.title = `You clicked ${count} times`;
// 函数 return 时等价于 componentWillUnmount 如解绑事件
return () => {
// do something...
};
},
// 监听数据,当依赖的值发生变更时,执行副作用函数
[ xxx,obj.xxx ]
);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect 的参数 useEffect有两个参数:一个回调函数和一个依赖数组
- 回调函数会在组件渲染时执行
- 依赖数组在指定的状态发生变化时重新执行回调函数
- 当数组为空 [ ],表示不会因为状态改变而执行回调方法,只会在在初始化时执行,相当于componentDidMount
- 当这个参数不传递,表示页面的任何状态一旦变更都会执行回调方法
- 当数组非空,数组里的值一旦有变化,就会执行回调方法
由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。
在每次渲染后调用副作用函数 —— 包括第一次渲染时,在 react 完成对 DOM 的渲染后执行useEffect
。
组件每渲染一次,该函数就自动执行一次,组件首次在 DOM 加载后,副作用函数也会执行。
Effect 的清除机制 useEffect 提供了一种清除副作用操作的机制,以避免内存泄漏或其他问题,且只在组件卸载前执行。当我们在使用 useEffect时,可以返回一个函数,这个函数会在组件卸载前执行,用于清除之前执行的副作用操作。
这意味着每次执行useEffect时,都会先清除之前的副作用操作,然后再执行新的副作用操作,如果组件多次渲染,则在执行下一个 useEffect 之前,上一个 useEffect 就已被清除。
useEffect(()=>{
// 右键监听事件
document.addEventListener('contextmenu', handleContext);
return ()=> {
// 移除事件监听
document.removeEventListener('contextmenu', handleContext)
}
})
useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。
userEffect 的用途
useEffect可以用于处理组件中的各种副作用操作:
- 数据获取:使用useEffect可以在组件渲染后发起网络请求,获取数据并更新组件状态。
- 订阅数据源:使用useEffect可以在组件渲染后订阅数据源,并在数据更新时更新组件状态。
- 手动修改DOM:使用useEffect可以在组件渲染后手动修改DOM,例如添加、删除或更新元素。
- 绑定监听事件:在 useEffect 中绑定事件的监听或订阅,并在 return 时解除对事件的绑定。
除此之外,useEffect还可以用于处理组件的其他逻辑:
- 组件初始化:使用useEffect可以在组件初始化时执行一些逻辑,例如设置初始状态、初始化数据等。
- 组件卸载:使用useEffect可以在组件卸载时执行一些清理逻辑,例如取消订阅、清除定时器等。
- 监听状态变化:使用useEffect可以监听组件状态的变化,并在变化时执行一些逻辑,例如更新DOM、触发动画等。
useEffect 是 React 中非常重要的一个 Hook,可以帮助我们处理各种副作用操作,并让函数组件具有类似于类组件的生命周期方法的功能。
useContext
实现跨组件间的数据传输
- 通过
createContext()
创建 MyContext 对象 并返回设置的初始值 - 父组件使用 <MyContext.Provider value={{xx:xx}}> 向后代组件传递信息,当 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,更新 value 值。
- 在子组件中使用 useContext(MyContext) 获取上下文
注意: useContext 的参数必须是 context 对象本身
- context-manager.js -- 定义 context
import {createContext} from 'react';
export const MyContext = createContext(null);
- useContext 的使用
import React, { useState } from 'react';
import Child from './Child';
import { MyContext } from './context-manager';
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
})
});
}
export default (props = {}) => {
const [step, setStep] = useState(0);
const [count, setCount] = useState(0);
const [number, setNumber] = useState(0);
return (
<MyContext.Provider value={{ setStep, setCount, setNumber, fetchData }}>
<Child step={step} number={number} count={count} />
</MyContext.Provider>
);
}
userReducer
userReducer 用于管理组件中的状态。与 useState 不同,useReducer 可以更好地处理复杂的状态逻辑,尤其是在需要多个状态变量相互作用时。
reducer 与 redux 的区别:
- reducer 用于单个组件的状态管理,适用于复杂的 state 变化
- redux 是全局状态管理,支持多组件间的共享数据
- 在组件间的通信还是使用props
reducer 接受两个参数一个是 state,另一个是 action,接收当前状态和 action 作为参数,并返回新的状态。
当组件需要更新状态时,可以通过调用 dispatch 函数来触发一个 action,从而更新状态。 如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。
import React,{useReducer} from 'react'
export default function Counter() {
const [count, dispath] = useReducer((state,action)=> {
switch(action){
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}, 0);
return (
<div>
<h1 className="title">{count}</h1>
<button className="btn is-primary"
onClick={()=> dispath('INCREMENT')}
>+</button>
<button className="btn is-warnning"
onClick={()=> dispath('DECREMENT')}
>-</button>
</div>
)
}
Counter 组件使用 useReducer 钩子来初始化计数器状态,并通过调用 dispatch 函数来更新状态。
count 是返回的状态值,当用户点击加号或减号按钮时,我们分别触发 INCREMENT 和 DECREMENT action,从而更新计数器的状态。
自定义Hook
自定义 Hook 是一个函数,其名称以 “use
” 开头,函数内部可以调用其他的 Hook。我们可以自由的决定它的参数,以及返回值,它像一个正常的函数,但是它的名字应该始终以 use
开头。
通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
定义Hook
在自定义组件中可以调用其他 Hook 如:useState, useEffect ,并支持在多个 Hook 之间传递信息 这里我们订阅一个名为 useFriendStatus 的 Hook:
import { useState, useEffect } from 'react';
// friendID 作为函数的参数
function useFriendStatus(friendID) {
// 设置属性
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
// 在组件初次渲染时(componentWillMount)、更新时(componentWillUpdate)调用
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
// return 的清除机制 在组件销毁时调用 ===>>> componentWillUnmount
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
// 返回好友在线的状态
return isOnline;
}
每个组件中使用的 Hook 都是相互隔离的,它们之间的状态不会共享或相互影响。
这是因为每次组件重新渲染时,React 都会创建一个新的 Hook 实例,并将其与组件实例相关联。因此,即使两个组件都使用相同的 Hook,它们也会拥有独立的状态和行为。
同时自定义 Hook 是一种重用状态逻辑的机制,每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。 两个组件获取到的 useState 相互隔离。
自定义Hook的使用
const friendList = [
{ id: 1, name: 'Phoebe' },
{ id: 2, name: 'Rachel' },
{ id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
// 通过 useState 初始化状态, 并返回更新状态的函数
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))} > <!-- 事件传参 -->
<!-- 循环渲染 list -->
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
当我们选择不同的好友并更新 recipientID
状态变量时,useFriendStatus
Hook 将会取消订阅之前选中的好友,并订阅新选中的好友状态。
useMemo
useMemo 能够将计算结果进行缓存,以提高组件的性能。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
它接收两个参数:一个计算函数和一个依赖项数组。
当依赖项数组中的任何一个值发生变化时,useMemo 将重新计算计算函数,并返回新的结果。否则,它将返回之前缓存的结果。
useMemo 拥有暂存能力,缓存上一次的结果并比对,若值未发生变化则不赋值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoizedValue。这种优化有助于避免在每次渲染时都进行高开销的计算。
注意:传入
useMemo
的函数会在渲染期间执行。所以不要在这个函数内部执行与渲染无关的操作。否则在每次渲染时 useMemo 都会进行计算。
可以把 useMemo
作为性能优化的手段,但不能保证其返回值在每次渲染中都是相等的。
useCallback
与 useMemo 类似 useCallback 也具有缓存功能,但不同的是 useCallback 是对函数进行缓存,它的作用在于利用缓存减少无效的更新渲染,以此来达到性能优化的作用。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的缓存版本,该回调函数只有在某个依赖项改变时才会更新。
当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
事实上,useCallback 内部实现就是使用了 useMemo,useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
,它们都可以用来缓存一个函数,并在依赖项发生变化时重新创建该函数。
使用 useMemo 来缓存一个回调函数:
import React, { useMemo } from 'react';
function MyComponent({ onClick }) {
const handleClick = useMemo(() => {
return () => {
// 处理点击事件
onClick();
};
}, [onClick]);
return <button onClick={handleClick}>Click me</button>;
}
handleClick 函数将被缓存,并且只有在 onClick 发生变化时才会重新创建该函数。这样可以避免在每次组件重新渲染时都重新创建函数实例,从而提高组件的渲染性能。
useMemo 与 useCallback 类似,都是有着缓存的作用,能够用于提高组件的性能。 本质的区别是useMemo 与 useCallback 类似它们缓存的内容不同:
- useMemo 是缓存计算结果,是一个值。
- useCallback 是缓存的是函数本身。
所以他们对应的使用场景也有所不同。
useRef
useRef 用于创建一个可变的 ref 对象。同时我们可以通过 ref 访问 DOM 与 createRef 不同的是:
useRef 在每次渲染时都会返回同一个 ref 对象,因此它可以用来存储组件中的持久化数据,而不会触发组件的重新渲染。
,useRef
会在每次渲染时返回同一个 ref 对象。
ref 的特性
- 返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数( initialValue )
- 返回的 ref 对象在组件的整个生命周期内保持不变
- 当更新 current 值时不会引发组件重新渲染
- 更新 useRef 是 side effect (副作用),所以一般写在 useEffect 或 event handler 里
- useRef 类似于类组件的 this
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
// 通过 ref 获取 dom 元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
在 useState 数据变更后 通过ref 获取dom元素
import React,{ useState, useRef, useEffect } from 'react'
const inputRef = useRef<HTMLInputElement>(null)
useEffect(()=>{
if(!active) {
inputRef.current?.focus()
}
}, [active])
useImperativeHandle
useImperativeHandle 用于暴露自定义的方法或属性给父组件,从而使父组件可以直接调用子组件中的方法或属性。
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 接受两个参数:ref 和一个回调函数。其中,ref 是一个由 React.forwardRef 创建的 ref 对象,用于在父组件中引用子组件。
在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与 forwardRef
一起使用:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input type="text" ref={inputRef} />;
});
回调函数接受两个参数:第一个参数是 ref 对象,第二个参数是一个返回值对象,该对象包含了需要暴露给父组件的方法或属性。
在上述的示例中我们通过 useImperativeHandle 在子组件中暴露了一个名为 focus 的方法。 在父组件中,我们可以通过引用子组件的 ref 对象来调用该方法:
import React, { useRef } from 'react';
import MyInput from './MyInput';
function MyForm() {
const inputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
inputRef.current.focus();
};
return (
<form onSubmit={handleSubmit}>
<MyInput ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
}
渲染 <MyInput ref={inputRef} />
的父组件可以通过 inputRef.current.focus()调用子组件中暴露的 focus()。
总结
以上就是我要介绍的一些关于 React 中常用的 几个较为重要的 Hook 啦,希望通过本文的学习能让你对Hook 有所了解并学会使用 Hook,从而提高组件的渲染性能。
那么今天就到这里啦,顺便记录一下这是 6 月更文的第 29 天啦,明天也要加油哦 !!!
参考
转载自:https://juejin.cn/post/7249299537068081210