一、Hook 介绍
Hooks 就是让函数组件“钩入”一些外部功能的函数,比如让函数组件拥有state、类似生命周期的特性。
1. 为什么用 hook?
-
方便从组件中提取状态逻辑以更好的复用:对于组件间复用状态逻辑,
React并没有提供好的方法,只能通过render props和高阶组件。而使用自定义hook可以从组件中很方便地提取状态逻辑,能够很好的复用。 -
解决class组件内逻辑难理解的问题:例如
componentDidMount中会有设置事件监听、发送网络请求等不相关的逻辑。而设置事件监听、移除事件监听这种相互关联的代码逻辑却被拆分。代码逻辑的不一致也会导致组件理解成本较高的问题。hook可以将不相关的逻辑拆分,将相关的逻辑放到一个hook中,使得代码逻辑更容易被理解、也更好测试。 -
class组件中的this指向,对于初学者来说是比较困扰的,在开发人员承接代码时也不容易理解维护。
2. 优缺点、缺点怎么解决
- 优点
- 更容易复用逻辑,通过自定义
hook。 - 代码的可读性较强、更容易做测试。
- 不用考虑
this指向问题。
- 更容易复用逻辑,通过自定义
- 缺点:对于
useEffect和useCallback这种有依赖项数组的,只有某个依赖项发生变化才会被调用。这些 hooks 的执行时机、依赖项设置比较难把握。 - 怎么改进缺点
- 不要在
useEffect里面写太多的依赖项,可以拆分成多个,更容易维护。 - 要加入
eslint-plugin-react-hooks这个插件。
- 不要在
3. 正确地使用
-
只在 React 函数的最顶层使用 Hook,不要在循环、条件或嵌套函数中调用 Hook。
一个组件中允许使用多个
useState、useEffect等 hooks,React 是根据 Hook 调用的顺序来将内部state和 Hook 关联。将 hook 放在函数的最顶层可以确保 Hook 的调用顺序在每一次渲染中都是相同的、保证 react 能够正常工作。 -
只在 React 函数中调用 Hook,不要在普通的 JS 函数中调用 Hook。能够确保组件的状态逻辑在代码中清晰可见。
- 可以在 React 的函数组件中调用 hook。
- 可以在自定义 hook 中调用其他 hook。
二、常见的 hooks
1. useState
useState 用来给组件添加 state。
- 唯一的参数就是初始 state,可以是任何类型(类组件里初始state只能是对象)。
- 返回值是一个数组:当前 state 和更新 state 的函数。
import { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0); // 初始state = 0
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
常见问题:
-
useState 和 setState 区别
- 一个用于函数组件,一个用于class组件;
- 参数形式:(参数),(参数1,参数2);
- 都是异步操作。setState 可以在第二个回调参数中拿到最新的 state,而 useState 可以借助 useEffect 实现。
-
hooks 中怎么获取更新后的 state
因为 useEffect 是在渲染后执行的,所以在 useEffect 中监听 state 的变化就可以获取到最新的 state。
2. useEffect
useEffect负责在函数组件中执行副作用。和class组件的生命周期函数相似,可以把它看作是componentDidMount、componentDidUpdate、componentWillUnmount的组合。
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 和 componentDidMount、componentDidUpdate类似,在 render 之后调用
useEffect(() => {
document.title = `You clicked ${count} times`; // 更新document.title
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
2.1 执行时机
useEffect 默认会在每次渲染后(挂载或更新后)执行。
每次渲染后都执行 effect 可能会导致性能问题,我们可以通过传入一个数组作为useEffect的第二个参数来指定 effect 的执行时机。React 会比较前后两个数组中的元素,如果没有变化就会跳过这个effect。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
- 如果只想运行一次 effect,那么可以传递一个空数组,相当于
componentDidMount! - 不传递第二个参数,那默认会在每次渲染后执行,相当于
componentDidMount加上componentDidUpdate。
2.2 如何清除副作用
useEffect 可以返回一个函数来执行清除操作, 相当于componentWillUnmount。
React 会在组件卸载时执行清除操作。但是因为 useEffect 会在每次渲染的时候都会执行,也就是先清除上一个 effect 然后执行当前的 effect。
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 返回一个清除函数,在组件卸载的时候执行清除操作
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
2.3 与 useLayoutEffect 的区别
useEffect 是在组件渲染完成之后执行的,是异步操作;useLayoutEffect 是在组件渲染之前执行的,是同步操作。
99% 的情况使用 useEffect 即可,只有当屏幕闪烁的时候再去用 useLayoutEffect。最好将操作 dom 的相关操作放到 useLayoutEffect 中。
3. useCallback
useCallback用来缓存一个函数。它的参数是需要缓存的回调函数和依赖项数组,返回值是该回调函数的memoized版本。该回调函数仅在某个依赖项改变时才会更新,否则就一直缓存第一个入参函数。
const memoizedCallback = useCallback(() => { fn, deps);
(补充)记忆函数:会缓存之前的计算结果,当下次调用遇到相同参数,就直接返回缓存的结果,不需要计算,仅适用于纯函数。
4. useMemo
useMemo用来缓存计算结果。它的参数是想要缓存的计算值(通常是函数返回值)和依赖项数组,返回一个 memoized值。仅当依赖项改变时才会执行参数函数,重新计算memoized值。
const memoizedValue = useMemo(() => fn, deps);
useCallback 和 useMemo 区别
两者都是一种性能优化的手段。在首次渲染时会执行一次,之后仅当依赖项数组改变时才会执行。
- 返回值:
useCallback返回值是一个缓存的回调函数;useMemo返回的是一个缓存值。 - 参数:前者是缓存函数和依赖项,后者是缓存值和依赖项。
- 使用场景:
- 当父组件传递一个函数作为 props 给子组件时,如果该函数用
useCallback缓存,那么当父组件更新时,子组件就可以跳过更新了、避免无效的 re-render。 useMemo可以缓存一些计算量很大的值,可以直接使用缓存的值,进而提升性能。
- 当父组件传递一个函数作为 props 给子组件时,如果该函数用
5. useReducer
用来给组件增加一个 reducer。reducer 是一个用于更新state的纯函数,返回值是新的state。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
//...
}
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
return (
<button onClick={() => dispatch({type: 'increment'})}>+</button>
);
}
和 useState 相比,useReducer 适用于复杂 state 的更新。
6. 自定义hook(了解)
通过自定义hook可以将组件间中的共有逻辑提取出来以便于进行复用。
- 自定义
hook是一个函数,名字必须以use开头(hook可以自动检查规则),函数内部可以调用其他的hook。 - 保证在自定义
hook的顶层调用其他hook。
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
- 在其他需要共享的组件中使用即可,
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
- 在两个组件之间使用相同的
hook并不会共享state,它们是完全隔离的。
7. 其他
- useRef:返回一个 ref 对象,其
.current属性就是初始传入的参数。和 state 不同,current 属性是可变的,而且发生改变时React不会重新渲染组件。 - useContext:用来读取和订阅 context。它会返回一个 context 值,这个值是由距离当前组件最近的上层组件的 Provider 决定的。当 context 变化时会重新渲染调用 useContext 的组件。
三、hook 与生命周期(了解)
hooks 应该能实现类里面所有的生命周期。
- constructor:useState
- getDerivedStateFromProps:
- render:函数本身
- componentDidMount:useEffect 第二个参数是空数组
- shouldComponentUpdate:memo()
- getSnapshotBeforeUpdate:
- componentDidUpdate:useEffect 配合 useRef
- componentWillUnmount:useEffect 里面返回的函数
四、共享组件间的状态逻辑
在一个大型应用程序中,很多组件内部的状态逻辑都是一样的,我们需要将这部分逻辑抽象出来,并在许多组件间共享它。
1. 高阶组件
高阶组件(HOC)是React中用于复用组件逻辑的一种技巧。高阶组件是参数为组件,返回值为新组件的函数。 例如Redux库的connnect。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
HOC是纯函数,它不会修改传入的组件,而是通过将组件进行包装来组成新组件。
和自定义hook的区别:
- 高阶组件的依赖项不清晰,是隐式的。
2. render props
组件是React代码复用的主要单元,但如何将一个组件封装的状态逻辑共享给其他需要相同状态逻辑的组件,解决方案比较少。