React Hooks学习指北

一、前言

在当今的前端开发环境中,越来越多的开发者认可了 Hooks 的强大能力,并纷纷加入到 Hooks 的使用大军中:

  • 2019 年 2 月,React 正式发布 v16.8 版本,引入 Hooks 能力(最新的 v18 中,还新增了 5 个 Hooks API);
  • 2019 年 6 月,尤雨溪提出了 Vue3 Composition API 的提案,使 Vue3 中也能够使用 Hooks;
  • 诸如 Ant Design Pro V5 等框架以及 Solid.jsPreact 等库,都选择将 Hooks 作为主体;
  • 很多优秀的开源项目(如 Ant Design)已经从原本的 Class 升级到使用 Hooks;

在 React v16.8 之前,我们主要使用 Class 组件,对函数组件的使用相对较少。这主要是因为函数组件虽然简洁,但由于缺乏数据状态管理,这一致命的缺陷使得 Class 组件成为主流选择。

引入 Hooks 后,带来了一系列优势:

  • 摆脱了繁琐的 super 传递;
  • 消除了 Class 组件中容易引发奇怪 this 指向的问题;
  • 摒弃了繁杂的生命周期方法。

此外,Hooks 提供了更好的状态复用。从强化组件模型的角度来看,我们可以发现自定义 Hooks 的模式与 mixin 模式更为相近。

为什么 mixin 会被废弃呢?其主要原因是 mixin 存在诸多弊端,其中一个显著的问题是引发了组件之间的耦合性增强。Mixin 模式使得组件之间共享状态和逻辑,但这也导致了一系列问题,例如:

  1. 命名冲突: 不同组件可能会定义相同名称的 mixin,从而造成命名冲突,使代码难以维护和理解。
  2. 复杂性增加: 随着 mixin 的引入,组件的复杂性呈指数增长。混合了多个 mixin 的组件往往难以追踪和调试,增加了代码维护的困难度。
  3. 难以追踪数据流: 组件的状态和逻辑被分散在多个 mixin 中,使得数据流难以追踪和理解。这增加了排查错误和进行性能优化的难度。
  4. 组件间耦合: 由于 mixin 的引入,组件之间的耦合性增强。一个组件可能会依赖于其他组件中定义的 mixin,导致组件之间的依赖关系错综复杂。
  5. 继承链问题: mixin 使用继承链来将逻辑注入到组件中,但这会导致不可预测的继承链问题,特别是在复杂的项目中。

总体而言,mixin 的弊端主要表现在引入了难以管理的复杂性、命名冲突、耦合性增强等方面,因此 React 官方明确表示不建议使用 mixin,而推荐采用更灵活、可维护的 Hooks 模式。Hooks 提供了更清晰、可组合的方式来处理组件的状态和逻辑,避免了 mixin 带来的诸多问题。

React 官方在提供 Hooks API 后,并没有强制要求开发者立刻转向使用它,而是通过明确 Hooks 的优势与劣势,让开发者自行选择。这种渐进的改变让项目中的开发者可以同时使用熟悉的 Class 组件和尝试新颖的 Hooks。随着项目的逐步迭代,开发者在实践中逐渐体会到 Hooks 的优势。这种悄无声息的变革使越来越多的开发者熟悉并纷纷加入 Hooks 的行列。

二、实战演练

主要演示v16提供的10种和v18中提供的5种 React Hooks API的使用

1. useState

useState: 定义变量,使其具备类组件的 state,让函数式组件拥有更新视图的能力。

基本使用:

const [state, setState] = useState(initData)

Params:

  • initData:默认初始值,有两种情况:函数和非函数,如果是函数,则函数的返回值作为初始值。

Result:

  • state:数据源,用于渲染UI 层的数据源;
  • setState:改变数据源的函数,可以理解为类组件的 this.setState

案例:

主要介绍两种setState的使用方法。

import { useState } from "react";
import { Button } from "antd";const Index = () => {const [count, setCount] = useState(0);return (<><div>数字:{count}</div><Button type="primary" onClick={() => setCount(count + 1)}>第一种方式+1</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => setCount((v) => v + 1)}>第二种方式+1</Button></>);
};export default Index;

注意: useState 有点类似于 PureComponent,它会进行一个比较浅的比较,这就导致了一个问题,如果是对象直接传入的时候,并不会实时更新,这点一定要切记。

我们做个简单的对比,比如:

import { useState } from "react";
import { Button } from "antd";const Index = () => {const [state, setState] = useState({ number: 0 });const [count, setCount] = useState(0);return (<><div>数字形式:{count}</div><Buttontype="primary"onClick={() => {setCount(count+1);}}>点击+1</Button><div>对象形式:{state.number}</div><Buttontype="primary"onClick={() => {state.number++;setState(state);}}>点击+1</Button></>);
};export default Index;

2. useEffect

useEffect: 副作用,这个钩子成功弥补了函数式组件没有生命周期的缺陷,是我们最常用的钩子之一。

基本使用:

useEffect(()=>{ return destory
}, deps)

Params:

  • callback:useEffect 的第一个入参,最终返回 destory,它会在下一次 callback 执行之前调用,其作用是清除上次的 callback 产生的副作用;
  • deps:依赖项,可选参数,是一个数组,可以有多个依赖项,通过依赖去改变,执行上一次的 callback 返回的 destory 和新的 effect 第一个参数 callback。

案例:

模拟挂载和卸载阶段

事实上,destory 会用在组件卸载阶段上,把它当作组件卸载时执行的方法就 ok,通常用于监听 addEventListenerremoveEventListener 上,如:

import { useState, useEffect } from "react";
import { Button } from "antd";const Child = () => {useEffect(() => {console.log("挂载");return () => {console.log("卸载");};}, []);return <div>react hooks!</div>;
};const Index = () => {const [flag, setFlag] = useState(false);return (<><Buttontype="primary"onClick={() => {setFlag((v) => !v);}}>{flag ? "卸载" : "挂载"}</Button>{flag && <Child />}</>);
};export default Index;

依赖变化:

dep的个数决定callback什么时候执行,如:

import { useState, useEffect } from "react";
import { Button } from "antd";const Index = () => {const [number, setNumber] = useState(0);const [count, setCount] = useState(0);useEffect(() => {console.log("count改变才会执行");}, [count]);return (<><div>number: {number} count: {count}</div><Button type="primary" onClick={() => setNumber((v) => v + 1)}>number + 1</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => setCount((v) => v + 1)}>count + 1</Button></>);
};export default Index;

无限执行:

当 useEffect 的第二个参数 deps 不存在时,会无限执行。更加准确地说,只要数据源发生变化(不限于自身中),该函数都会执行,所以请不要这么做,否则会出现不可控的现象。

import { useState, useEffect } from "react";
import { Button } from "antd";const Index = () => {const [count, setCount] = useState(0);const [flag, setFlag] = useState(false);useEffect(() => {console.log("hello hooks!");});return (<><Button type="primary" onClick={() => setCount((v) => v + 1)}>数字加一:{count}</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => setFlag((v) => !v)}>状态切换:{JSON.stringify(flag)}</Button></>);
};export default Index;

3. useContext

useContext: 上下文,类似于 Context,其本意就是设置全局共享数据,使所有组件可跨层级实现共享。

useContext 的参数一般是由 createContext 创建,或者是父级上下文 context传递的,通过 CountContext.Provider 包裹的组件,才能通过 useContext 获取对应的值。我们可以简单理解为 useContext 代替 context.Consumer 来获取 Provider 中保存的 value 值。

基本使用:

const contextValue = useContext(context)

Params:

  • context:一般而言保存的是 context 对象。

Result:

  • contextValue:返回的数据,也就是context对象内保存的value值。

案例:

子组件 Child 和孙组件 Son,共享父组件 Index 的数据 count。

import { useState, createContext, useContext } from "react";
import { Button } from "antd";const CountContext = createContext(-1);const Index = () => {const [count, setCount] = useState(0);return (<><div>父组件中的count:{count}</div><Button type="primary" onClick={() => setCount((v) => v + 1)}>点击+1</Button><CountContext.Provider value={count}><Child /></CountContext.Provider></>);
};const Child = () => {const countChild = useContext(CountContext);return (<div style={{ marginTop: 10 }}>子组件获取到的count: {countChild}<Son /></div>);
};const Son = () => {const countSon = useContext(CountContext);return <div style={{ marginTop: 10 }}>孙组件获取到的count: {countSon}</div>;
};export default Index;

4. useReducer

useReducer: 功能类似于 redux,与 redux 最大的不同点在于它是单个组件的状态管理,组件通讯还是要通过 props。简单地说,useReducer 相当于是 useState 的升级版,用来处理复杂的 state 变化。

基本使用:

const [state, dispatch] = useReducer((state, action) => {}, initialArg,init
);

Params:

  • reducer:函数,可以理解为 redux 中的 reducer,最终返回的值就是新的数据源 state;
  • initialArg:初始默认值;
  • init:惰性初始化,可选值。

Result:

  • state:更新之后的数据源;
  • dispatch:用于派发更新的dispatchAction,可以认为是useState中的setState

问:什么是惰性初始化?

答:惰性初始化是一种延迟创建对象的手段,直到被需要的第一时间才去创建,这样做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利。换句话说,如果有 init,就会取代 initialArg

案例:

import { useReducer } from "react";
import { Button } from "antd";const Index = () => {const [count, dispatch] = useReducer((state, action) => {switch (action?.type) {case "add":return state + action?.payload;case "sub":return state - action?.payload;default:return state;}}, 0);return (<><div>count:{count}</div><Buttontype="primary"onClick={() => dispatch({ type: "add", payload: 1 })}>1</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => dispatch({ type: "sub", payload: 1 })}>1</Button></>);
};export default Index;

特别注意: 在 reducer 中,如果返回的 state 和之前的 state 值相同,那么组件将不会更新。

比如这个组件是子组件,并不是组件本身,然后我们对上面的例子稍加更改,看看这个问题:

const Index = () => {console.log("父组件发生更新");...return (<>...<Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => dispatch({ type: "no", payload: 1 })}>无关按钮</Button><Child count={count} /></>)
};const Child = ({ count }) => {console.log("子组件发生更新");return <div>在子组件的count:{count}</div>;
};

可以看到,当 count 无变化时,子组件并不会更新。

5. useMemo

场景: 在每一次的状态更新中,都会让组件重新绘制,而重新绘制必然会带来不必要的性能开销,为了防止没有意义的性能开销,React Hooks 提供了 useMemo 函数。

useMemo:理念与 memo 相同,都是判断是否满足当前的限定条件来决定是否执行callback 函数。它之所以能带来提升,是因为在依赖不变的情况下,会返回相同的引用,避免子组件进行无意义的重复渲染。

基本使用:

const cacheData = useMemo(fn, deps)

Params:

  • fn:函数,函数的返回值会作为缓存值;
  • deps:依赖项,数组,会通过数组里的值来判断是否进行 fn 的调用,如果发生了改变,则会得到新的缓存值。

Result:

  • cacheData:更新之后的数据源,即 fn 函数的返回值,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的缓存值。

案例:

import { useState } from "react";
import { Button } from "antd";const usePow = (list) => {return list.map((item) => {console.log("我是usePow");return Math.pow(item, 2);});
};const Index = () => {const [flag, setFlag] = useState(true);const data = usePow([1, 2, 3]);return (<><div>数字集合:{JSON.stringify(data)}</div><Button type="primary" onClick={() => setFlag((v) => !v)}>状态切换{JSON.stringify(flag)}</Button></>);
};export default Index;

从例子中来看, 按钮切换的 flag 应该与 usePow 的数据毫无关系,

可以看到,当我们点击按钮后,会打印我是usePow,这样就会产生开销。毫无疑问,这种开销并不是我们想要见到的结果,所以有了 useMemo。 并用它进行如下改造:

const usePow = (list) => {return useMemo(() =>list.map((item) => {console.log(1);return Math.pow(item, 2);}),[]);
};

6. useCallback

useCallback:与 useMemo 极其类似,甚至可以说一模一样,唯一不同的点在于,useMemo 返回的是值,而 useCallback 返回的是函数。

基本使用:

const resfn = useCallback(fn, deps)

Params:

  • fn:函数,函数的返回值会作为缓存值;
  • deps:依赖项,数组,会通过数组里的值来判断是否进行 fn 的调用,如果依赖项发生改变,则会得到新的缓存值。

Result:

  • resfn:更新之后的数据源,即 fn 函数,如果 deps 中的依赖值发生改变,将重新执行 fn,否则取上一次的函数。

案例:

import { useState, useCallback, memo } from "react";
import { Button } from "antd";const Index = () => {let [count, setCount] = useState(0);let [flag, setFlag] = useState(true);const add = useCallback(() => {setCount(count + 1);}, [count]);return (<><TestButton onClick={() => setCount((v) => v + 1)}>普通点击</TestButton><TestButton onClick={add}>useCallback点击</TestButton><div>数字:{count}</div><Button type="primary" onClick={() => setFlag((v) => !v)}>切换{JSON.stringify(flag)}</Button></>);
};const TestButton = memo(({ children, onClick = () => {} }) => {console.log(children);return (<Buttontype="primary"onClick={onClick}style={children === "useCallback点击" ? { marginLeft: 10 } : undefined}>{children}</Button>);
});export default Index;

简要说明下,TestButton 里是个按钮,分别存放着有无 useCallback 包裹的函数,在父组件 Index 中有一个 flag 变量,这个变量同样与 count 无关,那么,我们切换按钮的时候,TestButton 会怎样执行呢?

可以看到,我们切换 flag 的时候,没有经过 useCallback 的函数会再次执行,而包裹的函数并没有执行(点击“普通点击”按钮的时候,useCallbak 的依赖项 count 发生了改变,所以会打印出 useCallback 点击)。

7. useRef

useRef: 用于获取当前元素的所有属性,除此之外,还有一个高级用法:缓存数据。

基本使用:

const ref = useRef(initialValue);

Params:

  • initialValue:初始值,默认值。

Result:

  • ref:返回的一个 current 对象,这个 current 属性就是 ref 对象需要获取的内容。

案例:

import { useState, useRef } from "react";const Index = () => {const scrollRef = useRef(null);const [clientHeight, setClientHeight] = useState(0);const [scrollTop, setScrollTop] = useState(0);const [scrollHeight, setScrollHeight] = useState(0);const onScroll = () => {if (scrollRef?.current) {let clientHeight = scrollRef?.current.clientHeight; //可视区域高度let scrollTop = scrollRef?.current.scrollTop; //滚动条滚动高度let scrollHeight = scrollRef?.current.scrollHeight; //滚动内容高度setClientHeight(clientHeight);setScrollTop(scrollTop);setScrollHeight(scrollHeight);}};return (<><div><p>可视区域高度:{clientHeight}</p><p>滚动条滚动高度:{scrollTop}</p><p>滚动内容高度:{scrollHeight}</p></div><divstyle={{ height: 200, border: "1px solid #000", overflowY: "auto" }}ref={scrollRef}onScroll={onScroll}><div style={{ height: 2000 }}></div></div></>);
};export default Index;

8. useImperativeHandle

useImperativeHandle:可以通过 ref 或 forwardRef 暴露给父组件的实例值,所谓的实例值是指值和函数。

实际上这个钩子非常有用,简单来讲,这个钩子可以让不同的模块关联起来,让父组件调用子组件的方法。

举个例子,在一个页面很复杂的时候,我们会将这个页面进行模块化,这样会分成很多个模块,有的时候我们需要在最外层的组件上控制其他组件的方法,希望最外层的点击事件同时执行子组件的事件,这时就需要 useImperativeHandle 的帮助(在不用redux等状态管理的情况下)。

基本使用:

useImperativeHandle(ref, createHandle, deps)

Params:

  • ref:接受 useRef 或 forwardRef 传递过来的 ref;
  • createHandle:处理函数,返回值作为暴露给父组件的 ref 对象;
  • deps:依赖项,依赖项如果更改,会形成新的 ref 对象。

案例:

父组件是函数式组件:

import { useState, useRef, useImperativeHandle } from "react";
import { Button } from "antd";const Child = ({cRef}) => {const [count, setCount] = useState(0)useImperativeHandle(cRef, () => ({add}))const add = () => {setCount((v) => v + 1)}return <div><p>点击次数:{count}</p><Button onClick={() => add()}> 子组件的按钮,点击+1</Button></div>
}const Index = () => {const ref = useRef<any>(null)return (<><div>hello hooks!</div><div></div><Buttontype="primary"onClick={() =>  ref.current.add()}>父组件上的按钮,点击+1</Button><Child cRef={ref} /></>);
};export default Index;

当父组件是类组件时:

如果当前的父组件是 Class 组件,此时不能使用 useRef,而是需要用 forwardRef 来协助我们处理。

forwardRef:引用传递,是一种通过组件向子组件自动传递引用 ref 的技术。对于应用者的大多数组件来说没什么作用,但对于一些重复使用的组件,可能有用。

经过 forwardRef 包裹后,会将 props(其余参数)和 ref 拆分出来,ref 会作为第二个参数进行传递。如:

import { useState, useRef, useImperativeHandle, Component, forwardRef } from "react";
import { Button } from "antd";const Child = (props, ref) => {const [count, setCount] = useState(0)useImperativeHandle(ref, () => ({add}))const add = () => {setCount((v) => v + 1)}return <div><p>点击次数:{count}</p><Button onClick={() => add()}> 子组件的按钮,点击+1</Button></div>
}const ForwardChild = forwardRef(Child)class Index extends Component{countRef = nullrender(){return   <><div>hello hooks!</div><div></div><Buttontype="primary"onClick={() => this.countRef.add()}>父组件上的按钮,点击+1</Button><ForwardChild ref={node => this.countRef = node} /></>}
}export default Index;

9. useLayoutEffect

useLayoutEffect: 与 useEffect 基本一致,不同点在于它是同步执行的。简要说明:

  • 执行顺序:useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前的操作,这样可以更加方便地修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,所以 useLayoutEffect 的执行顺序在 useEffect 之前;
  • useLayoutEffect 相当于有一层防抖效果;
  • useLayoutEffect 的 callback 中会阻塞浏览器绘制。

基本使用:

useLayoutEffect(callback,deps)

案例:

防抖效果:

import { useState, useEffect, useLayoutEffect } from "react";const Index = () => {const [count, setCount] = useState(0);const [count1, setCount1] = useState(0);useEffect(() => {if(count === 0){setCount(10 + Math.random() * 100)}}, [count])useLayoutEffect(() => {if(count1 === 0){setCount1(10 + Math.random() * 100)}}, [count1])return (<><div>hello Hooks!</div><div>useEffect的count:{count}</div><div>useLayoutEffect的count:{count1}</div></>);
};export default Index;

在这个例子中,我们分别设置 count 和 count1 两个变量,初始值都为 0,然后分别通过 useEffect 和 useLayout 控制,通过随机值来变更两个变量的值。也就是说,count 和 count1 连续变更了两次。

从结果上来看,count 要比 count1 更加抖动。

这是因为两者的执行顺序,简要分析下:

  • useEffect 执行顺序:setCount 设置 => 在 DOM 上渲染 => useEffect 回调 => setCount 设置 => 在 DOM 上渲染。
  • useLayoutEffect 执行顺序:setCount 设置 => useLayoutEffect 回调 => setCount 设置 => 在 DOM 上渲染。

可以看出,useEffect 实际进行了两次渲染,这样就可能导致浏览器再次回流和重绘,增加了性能上的损耗,从而会有闪烁突兀的感觉。

10. useDebugValue

useDebugValue: 可用于在 React 开发者工具中显示自定义 Hook 的标签。这个 Hooks 目的就是检查自定义 Hooks。

注意: 这个标签并不推荐向每个 hook 都添加 debug 值。当它作为共享库的一部分时才最有价值。(也就是自定义 Hooks 被复用的值)。因为在一些情况下,格式化值可能是一项开销很大的操作,除非你需要检查 Hook,否则没有必要这么做。

基本使用:

useDebugValue(value, (status) => {})

Params:

  • value:判断的值;
  • callback:可选,这个函数只有在 Hook 被检查时才会调用,它接受 debug 值作为参数,并且会返回一个格式化的显示值。

案例:

function useFriendStatus(friendID) {const [isOnline, setIsOnline] = useState(null);// ...// 在开发者工具中的这个 Hook 旁边显示标签  // e.g. "FriendStatus: Online"  useDebugValue(isOnline ? 'Online' : 'Offline');return isOnline;
}

11. useSyncExternalStore

useSyncExternalStore: 会通过强制的同步状态更新,使得外部 store 可以支持并发读取。

注意: 这个 Hooks 并不是在日常开发中使用的,而是给第三方库 reduxmobx 使用的,因为在 React v18 中,主推的 Concurrent(并发)模式可能会出现状态不一致的问题(比如在 react-redux 7.2.6 的版本),所以官方给出 useSyncExternalStore 来解决此类问题。

简单地说,useSyncExternalStore 能够让 React 组件在 Concurrent 模式下安全、有效地读取外接数据源,在组件渲染过程中能够检测到变化,并且在数据源发生变化的时候,能够调度更新。

当读取到外部状态的变化,会触发强制更新,以此来保证结果的一致性。

基本使用:

const state = useSyncExternalStore(subscribe,getSnapshot,getServerSnapshot
)

Params:

  • subscribe:订阅函数,用于注册一个回调函数,当存储值发生更改时被调用。 此外,useSyncExternalStore 会通过带有记忆性的 getSnapshot 来判断数据是否发生变化,如果发生变化,那么会强制更新数据;
  • getSnapshot:返回当前存储值的函数。必须返回缓存的值。如果 getSnapshot 连续多次调用,则必须返回相同的确切值,除非中间有存储值更新;
  • getServerSnapshot:返回服务端(hydration 模式下)渲染期间使用的存储值的函数。

Result:

  • state:数据源,用于渲染 UI 层的数据源。

案例:

import { useSyncExternalStore } from "react";
import { Button } from "antd";
import { combineReducers, createStore } from "redux";const reducer = (state = 1, action) => {switch (action.type) {case "ADD":return state + 1;case "DEL":return state - 1;default:return state;}
};/* 注册reducer,并创建store */
const rootReducer = combineReducers({ count: reducer });
const store = createStore(rootReducer, { count: 1 });const Index = () => {//订阅const state = useSyncExternalStore(store.subscribe,() => store.getState().count);return (<><div>Hooks!</div><div>数据源: {state}</div><Button type="primary" onClick={() => store.dispatch({ type: "ADD" })}>1</Button><Buttonstyle={{ marginLeft: 8 }}onClick={() => store.dispatch({ type: "DEL" })}>1</Button></>);
};export default Index;

当我们点击按钮后,会触发 store.subscribe(订阅函数),执行 getSnapshot 后得到新的 count,此时 count 发生变化,就会触发更新。

12. useTransition

useTransition: 返回一个状态值表示过渡更新任务的等待状态,以及一个启动该过渡更新任务的函数。

问:什么是过渡更新任务?

答:过渡任务是对比紧急更新任务所产生的。

紧急更新任务指,输入框、按钮等任务需要在视图上立即做出响应,让用户立马能够看到效果的任务。

但有时,更新任务不一定那么紧急,或者说需要去请求数据,导致新的状态不能够立马更新,需要一个 loading... 的状态,这类任务称为过渡任务。

我们再来举个比较常见的例子帮助理解紧急更新任务和过渡更新任务。

当我们有一个 input 输入框,这个输入框的值要维护一个很大列表(假设列表有 1w 条数据),比如说过滤、搜索等情况,这时有两种变化:

  1. input 框内的变化;
  2. 根据 input 的值,1w 条数据的变化。

input 框内的变化是实时获取的,也就是受控的,此时的行为就是紧急更新任务。而这 1w 条数据的变化,就会有过滤、重新渲染的情况,此时这种行为被称为过渡更新任务。

基本使用:

const [isPending, startTransition] = useTransition();

Result:

  • isPending:布尔值,过渡状态的标志,为 true 时表示等待状态;
  • startTransition:可以将里面的任务变成过渡更新任务。

案例:

import { useState, useTransition } from "react";
import { Input } from "antd";const Index = () => {const [isPending, startTransition] = useTransition();const [input, setInput] = useState("");const [list, setList] = useState([]);return (<><div>Hooks!</div><Inputvalue={input}onChange={(e) => {setInput(e.target.value);startTransition(() => {const res = [];for (let i = 0; i < 10000; i++) {res.push(e.target.value);}setList(res);});}}/>{isPending ? (<div>加载中...</div>) : (list.map((item, index) => <div key={index}>{item}</div>))}</>);
};export default Index;

从上述的代码可以看到,我们通过 input 去维护了 1w 条数据,通过 isPending 的状态来控制是否展示完成。

13. useDeferredValue

useDeferredValue:可以让状态滞后派生,与 useTransition 功能类似,推迟屏幕优先级不高的部分。

在一些场景中,渲染比较消耗性能,比如输入框。输入框的内容去调取后端服务,当用户连续输入的时候会不断地调取后端服务,其实很多的片段信息是无用的,这样会浪费服务资源, React 的响应式更新和 JS 单线程的特性也会导致其他渲染任务的卡顿。而 useDeferredValue 就是用来解决这个问题的。

问:useDeferredValue 和 useTransition 怎么这么相似,两者有什么异同点?

答:useDeferredValue 和 useTransition 从本质上都是标记成了过渡更新任务,不同点在于 useDeferredValue 是将原值通过过渡任务得到新的值, 而 useTransition 是将紧急更新任务变为过渡任务。

也就是说,useDeferredValue 用来处理数据本身,useTransition 用来处理更新函数。

基本使用:

const deferredValue = useDeferredValue(value);

Params:

  • value:接受一个可变的值,如useState所创建的值。

Result:

  • deferredValue:返回一个延迟状态的值。

案例:

import { useState, useDeferredValue } from "react";
import { Input } from "antd";const getList = (key) => {const arr = [];for (let i = 0; i < 10000; i++) {if (String(i).includes(key)) {arr.push(<li key={i}>{i}</li>);}}return arr;
};const Index = () => {//订阅const [input, setInput] = useState("");const deferredValue = useDeferredValue(input);console.log("value:", input);console.log("deferredValue:", deferredValue);return (<><div>Hooks!</div><Input value={input} onChange={(e) => setInput(e.target.value)} /><div><ul>{deferredValue ? getList(deferredValue)}</ul></div></>);
};export default Index;

上述的功能类似于搜索,从 1w 个数中找到输入框内的数。

问:什么场景下使用useDeferredValueuseTransition

答:通过上面的两个例子介绍我们知道,useDeferredValue 和 useTransition 实际上都是用来处理数据量大的数据,比如,百度输入框、散点图等,都可以使用。它们并不适用于少量数据。

但在这里更加推荐使用 useTransition,因为 useTransition 的性能要高于 useDeferredValue,除非像一些第三方的 Hooks 库,里面没有暴露出更新的函数,而是直接返回值,这种情况下才去考虑使用 useDeferredValue。

这两者可以说是一把双刃剑,在数据量大的时候使用会优化性能,而数据量低的时候反而会影响性能。

14. useInsertionEffect

useInsertionEffect: 与 useEffect 一样,但它在所有 DOM 突变之前同步触发。

注意:

  • useInsertionEffect 应限于 css-in-js 库作者使用。在实际的项目中优先考虑使用 useEffect 或 useLayoutEffect 来替代;
  • 这个钩子是为了解决 CSS-in-JS 在渲染中注入样式的性能问题而出现的,所以在我们日常的开发中并不会用到这个钩子,但我们要知道如何去使用它。

基本使用:

useInsertionEffect(callback,deps)

案例:

import { useInsertionEffect } from "react";const Index = () => {useInsertionEffect(() => {const style = document.createElement("style");style.innerHTML = `.css-in-js{color: blue;}`;document.head.appendChild(style);}, []);return (<div><div className="css-in-js">,一起学Hooks吧!</div></div>);
};export default Index;

执行顺序: 在目前的版本中,React 官方共提供三种有关副作用的钩子,分别是 useEffect、useLayoutEffect 和 useInsertionEffect,我们一起来看看三者的执行顺序:

import { useEffect, useLayoutEffect, useInsertionEffect } from "react";const Index = () => {useEffect(() => console.log("useEffect"), []);useLayoutEffect(() => console.log("useLayoutEffect"), []);useInsertionEffect(() => console.log("useInsertionEffect"), []);return <div>,Hooks!</div>;
};export default Index;

从效果上来看,可知三者的执行的顺序为:useInsertionEffect > useLayoutEffect > useEffect。

15. useId

useId: 是一个用于生成横跨服务端和客户端的稳定的唯一 ID ,用于解决服务端与客户端产生 ID 不一致的问题,更重要的是保证了 React v18 的 streaming renderer (流式渲染)中 id 的稳定性。

这里我们简单介绍一下什么是 streaming renderer

在之前的 React ssr 中,hydrate( 与 render 相同,但作用于 ReactDOMServer 渲染的容器中 )是整个渲染的,也就是说,无论当前模块有多大,都会一次性渲染,无法局部渲染。但这样就会有一个问题,如果这个模块过于庞大,请求数据量大,耗费时间长,这种效果并不是我们想要看到的。

于是在 React v18 上诞生出了 streaming renderer (流式渲染),也就是将整个模块进行拆分,让加载快的小模块先进行渲染,大的模块挂起,再逐步加载出大模块,就可以就解决上面的问题。

此时就有可能出现:服务端和客户端注册组件的顺序不一致的问题,所以 useId 就是为了解决此问题而诞生的,这样就保证了 streaming renderer 中 ID 的稳定性。

基本使用:

const id = useId();

Result:

  • id:生成一个服务端和客户端统一的id

案例:

import { useId } from "react";const Index = () => {const id = useId();return <div id={id}>一起学Hooks吧!</div>;
};export default Index;

三、自定义hooks

什么是自定义hooks

自定义hooks是在react-hooks基础上的一个拓展,可以根据业务需要制定满足业务需要的hooks,更注重的是逻辑单元。通过业务场景不同,我们到底需要react-hooks做什么,怎么样把一段逻辑封装起来,做到复用,这是自定义hooks产生的初衷。

如何设计一个自定义hooks,设计规范

逻辑 + 组件

hooks 专注的就是逻辑复用, 我们的项目,不仅仅停留在组件复用的层面上。hooks让我们可以将一段通用的逻辑存封起来。将我们需要它的时候,开箱即用即可。

1.驱动条件

hooks本质上是一个函数。函数的执行,决定于无状态组件自身的执行上下文。每次函数的执行(本质上就是组件的更新)就是执行自定义hooks的执行,由此可见组件本身执行和hooks的执行如出一辙。

那么prop的修改,useState,useReducer使用是无状态组件更新条件,那么就是驱动hooks执行的条件。

2.通用模式

我们设计的自定义react-hooks应该是长的这样的。

const [ xxx , ... ] = useXXX(参数A,参数B...)

在我们在编写自定义hooks的时候,要特别~特别关注的是传进去什么返回什么。 返回的东西是我们真正需要的。更像一个工厂,把原材料加工,最后返回我们。

3.条件限定

如果自定义hooks没有设计好,比如返回一个改变state的函数,但是没有加条件限定,就有可能造成不必要的上下文的执行,更有甚的是导致组件的循环渲染执行。

比如:我们写一个非常简单hooks来格式化数组将小写转成大写

import React , { useState } from 'react'
/* 自定义hooks 用于格式化数组将小写转成大写 */
function useFormatList(list){return list.map(item=>{return item.toUpperCase()})
}
/* 父组件传过来的list = [ 'aaa' , 'bbb' , 'ccc'  ] */
function index({ list }){const [ number ,setNumber ] = useState(0)const newList = useFormatList(list)return <div><div className="list" >{ newList.map(item=><div key={item} >{ item }</div>) }</div><div className="number" ><div>{ number }</div><button onClick={()=> setNumber(number + 1) } >add</button></div></div>
}
export default index

上述问题,我们格式化父组件传递过来的list数组,并将小写变成大写,但是当我们点击add。 理想状态下数组不需要重新format,但是实际跟着执行format。无疑增加了性能开销。

所以我们在设置自定义hooks的时候,一定要把条件限定-性能开销加进去。

于是乎我们这样处理一下。

function useFormatList(list) {return useMemo(() => list.map(item => {return item.toUpperCase()}), [])
}

所以一个好用的自定义hooks,一定要配合useMemo, useCallbackapi一起使用。

第三方hooks库推荐:

ahooks 是由蚂蚁 umi 团队、淘系 ice 团队以及阿里体育团队共同建设的 React Hooks 工具库。ahooks 基于 React Hooks 的逻辑封装能力,提供了大量常见好用的 Hooks,可以极大降低代码复杂度,提升开发效率。

四、小结

hook名称功能
useState定义变量,使其具备类组件的state,让函数组件更新视图的能力
useEffect副作用,这个钩子成功弥补了函数式组件没有生命周期的缺陷
useContext上下文,类似于Context,其本意就是设置全局共享数据,使所有组件可跨层级实现共享
useReducer功能类似于redux,于redux最大的不同点是在于它是单个组件的状态管理,组件通讯还是要通过props,是一种useState的升级版,处理复杂的state变化
useMemo理念与memo相同,都是判断是否满足当前的限定条件来决定是否执行callback函数
useCallback与useMemo类似,甚至可以说一模一样,唯一不同的点在于,useMemo返回的是值,而useCallback返回的函数
useRef用于获取当前元素的所有属性,除此之外,还有一个高级用法:缓存数据
useImperativeHandle可以通过ref或forwardRef暴露给父组件的实例值,所谓的实例值是指值和函数
useLayoutEffect与useEffect基本一致,不同点在于它是同步执行的
useDebugValue可用于在React开发者工具中显示自定义hook的标签。这个hooks目的就是检查自定义hooks
useSyncExternalStore会通过强制的同步状态更新,使得外部store可以支持并发读取
useTransition返回一个状态值表示过渡更新任务的等待状态,以及一个启动该过渡更新任务的函数
useDeferredValue可以让状态滞后派生,与useTransition 类似,允许用户推迟屏幕更新优先级不分高低
useInsertionEffect与useEffect一样,但它在所有DOM突变之前同步触发
useId一个用于生成横跨服务端和客户端的稳定的唯一ID,用于解决了服务端与客户端产生ID不一致的问题

参考链接:

  • https://mp.weixin.qq.com/s/TovRZ-SsUaeLplCVSvhnpg
  • https://juejin.cn/post/6890738145671938062?searchId=20231102205318349AE1907161FD35C254
  • https://juejin.cn/post/6944863057000529933?searchId=20231102211152FB4D52BD86A7BA3B6CDE
  • https://juejin.cn/post/7236158655128125498

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。

关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/215654.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

移液器吸头材质选择——PFA吸头在半导体化工行业的应用

PFA吸头是一种高性能移液器配件&#xff0c;这种材料具有优异的耐化学品、耐热和电绝缘性能&#xff0c;使得PFA吸头在应用中表现出色。那么它有哪些特点呢&#xff1f; 首先&#xff0c;PFA吸头具有卓越的耐化学腐蚀性能。无论是酸性溶液、碱性溶液还是有机溶剂&#xff0c;P…

如何用CHAT帮你提高工作效率?

问CHAT&#xff1a;从规范项目管理流程交付&#xff0c;分别对项目信息安全管理&#xff0c;项目预算管理和项目采购管理三个方面提建议 CHAT回复&#xff1a; 项目信息安全管理: 1. 制定详细的信息安全政策&#xff0c;所有参与项目的员工必须遵守&#xff0c;对其中涉及敏感…

wpf TelerikUI使用DragDropManager

首先&#xff0c;我先创建事务对象ApplicationInfo&#xff0c;当暴露出一对属性当例子集合对于构成ListBoxes。这个类在例子中显示如下代码&#xff1a; public class ApplicationInfo { public Double Price { get; set; } public String IconPath { get; set; } public …

亚马逊S3V4验签与MINIO验签区别

1、先看下官方文档 AWS S3V4 DEMO 2、实际调用试试 1&#xff09;代码 // 计算auth// for a simple GET, we have no body so supply the precomputed empty hashMap<String, String> headers new HashMap<String, String>();headers.put("x-amz-content…

0013Java安卓程序设计-ssm酒品移动电商平台app

文章目录 **摘要**目录系统实现5.1 APP端5.2管理员功能模块开发环境 编程技术交流、源码分享、模板分享、网课分享 企鹅&#x1f427;裙&#xff1a;776871563 摘要 首先,论文一开始便是清楚的论述了系统的研究内容。其次,剖析系统需求分析,弄明白“做什么”,分析包括业务分析…

Firewalld 防火墙配置

文章目录 Firewalld 防火墙配置1. Firewalld 概述2. 区域名称及策略规则3. Firewalld 配置方法4. Firewalld 参数和命令5. Firewalld 两种模式6. Firewalld 使用 Firewalld 防火墙配置 1. Firewalld 概述 firewalld 是一个动态防火墙管理器&#xff0c;作为 Systemd 管理的防…

【docker】常用命令

启动docker服务 systemctl start docker 停止docker服务 systemctl stop docker 重启docker服务 systemctl restart docker 查看docker服务状态 systemctl status docker 设置开机启动docker服务 systemctl enable docker 设置关闭开机启动docker服务 systemctl disable …

数据在内存中的存储(浮点型篇)

1.例子&#xff1a;5.5&#xff1a;内存存储为101.1&#xff0c;十分位百分位依次为2的-1次方&#xff0c;2的-2次方&#xff0c;而使用科学计数法可以改写为1.011*2的2次方 2.国际标准公式&#xff1a;-1的D次方*M*2的E次方&#xff0c;x1负0正 3.M在存储时默认整数部分为1&…

使用Spring Boot和领域驱动设计实现模块化整体

用模块化整体架构编写的代码实际上是什么样的&#xff1f;借助 Spring Boot 和 DDD&#xff0c;我们踏上了编写可维护和可演化代码的旅程。 当谈论模块化整体代码时&#xff0c;我们的目标是以下几点&#xff1a; 应用程序被组织成模块。每个模块解决业务问题的不同部分。模块…

springcloud微服务篇--1.认识微服务

一、服务架构演变。 单体架构&#xff1a; 将业务的所有功能集中在一个项目中开发&#xff0c;打成一个包部署。 优点&#xff1a;架构简单 &#xff0c;部署成本低。 缺点&#xff1a;耦合度高 分布式架构 根据业务功能对系统进行拆分&#xff0c;每个业务模块作为独立项…

[idea]idea连接clickhouse23.6.2.18

一、安装驱动 直接在pom.xml加上那个lz4也是必要的不然会报错 <dependency><groupId>com.clickhouse</groupId><artifactId>clickhouse-jdbc</artifactId><version>0.4.2</version></dependency><dependency><group…

歌唱比赛计分 (8 分)设有10名歌手(编号为1-10)参加歌咏比赛

未采用结构体的解法&#xff0c;通过二维数组解题 #include <stdio.h> void rank(int arr[10][6] ) { int str[4] { 0 }; int a1[6] { 0 }; int k 0; int i 0; int z 0; int j 0; int temp 0; double s1[10][2] { 0 }; dou…

(1)mysql容器化部署

mysql容器化部署&#xff1a; 数据持久化&#xff08;方便数据保存及迁移&#xff09;: 需要持久化两个目录: 创建/mysql (1)mysql配置文件: /mysql/mysql-cnf/my.cnf vim my.cnf [mysqld] pid-file /var/run/mysqld/mysqld.pid socket /var/run/mysqld/…

【51单片机系列】使用74HC595控制数码管显示

使用74HC595结合数码管显示字符。 proteus仿真设计如下&#xff0c;74HC595的输出端连接到动态数码管的位选和静态数码管的段选&#xff0c;动态数码管的段选连接到P0口。这两个数码管都是共阴极的。 静态数码管显示字符0-F&#xff0c;软件设计如下&#xff1a; /*实现功能&a…

Java:SpringBoot获取当前运行的环境activeProfile

代码示例 /*** 启动监听器*/ Component public class AppListener implements ApplicationListener<ApplicationReadyEvent> {Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// 获取当前的环境&#xff0c;如果是test&#xff0c;则直接返回Co…

redis实际应用实现合集

一、redis实现抢红包的功能&#xff08;set 数据结构&#xff09; 分两种情况&#xff1a; 情况一: 从10个观众中随机抽2名幸运观众 首先需要把10个观众的id&#xff08;具体是什么id可以根据实际业务情况自己定义&#xff09;放到redis 的 set 集合里 然后随机抽取2名幸运…

【hcie-cloud】【8】华为云Stack_LLD设计【部署设计、资源设计、服务设计、学习推荐、缩略语】【下】

设计概览、整体架构设计、网络设计 看下面-这篇文章 【hcie-cloud】【7】华为云Stack_LLD设计【设计概览、整体架构设计、网络设计、部署设计、资源设计、服务设计】【上】 部署设计 云平台整体部署架构 图中在Region下每个灰底都代表一个数据中心&#xff0c;AZ1可以跨数据…

yarn系统架构与安装

1.1 YARN系统架构 YARN的基本思想是将资源管理和作业调度/监视功能划分为单独的守护进程。其思想是拥有一个全局ResourceManager (RM)&#xff0c;以及每个应用程序拥有一个ApplicationMaster (AM)。应用程序可以是单个作业&#xff0c;也可以是一组作业。 一个ResourceManage…

ai智能机器人外呼系统怎么操作?

什么是ai智能机器人外呼&#xff1f;ai智能机器人外呼怎么操作&#xff1f;当下&#xff0c;很多企业主已经认识到&#xff0c;AI外呼是一种高效的拉新引流手段。但具体到实际应用中&#xff0c;实现的效果好像并没有那么理想。从企业外呼的结果来看&#xff0c;接通率是可以达…