找回密码
 会员注册
查看: 25|回复: 0

ReactHooks学习指北

[复制链接]

4

主题

0

回帖

13

积分

新手上路

积分
13
发表于 2024-9-19 17:17:49 | 显示全部楼层 |阅读模式
在当今的前端开发环境中,越来越多的开发者认可了Hooks的强大能力,并纷纷加入到Hooks的使用大军中:2019年2月,React正式发布v16.8版本,引入Hooks能力(最新的v18中,还新增了5个HooksAPI);2019年6月,尤雨溪提出了Vue3CompositionAPI的提案,使Vue3中也能够使用Hooks;诸如AntDesignProV5等框架以及Solid.js、Preact等库,都选择将Hooks作为主体;很多优秀的开源项目(如AntDesign)已经从原本的Class升级到使用Hooks;......在Reactv16.8之前,我们主要使用Class组件,对函数组件的使用相对较少。这主要是因为函数组件虽然简洁,但由于缺乏数据状态管理,这一致命的缺陷使得Class组件成为主流选择。引入Hooks后,带来了一系列优势:摆脱了繁琐的super传递;消除了Class组件中容易引发奇怪this指向的问题;摒弃了繁杂的生命周期方法。此外,Hooks提供了更好的状态复用。从强化组件模型的角度来看,我们可以发现自定义Hooks的模式与mixin模式更为相近。为什么mixin会被废弃呢?其主要原因是mixin存在诸多弊端,其中一个显著的问题是引发了组件之间的耦合性增强。Mixin模式使得组件之间共享状态和逻辑,但这也导致了一系列问题,例如:命名冲突:不同组件可能会定义相同名称的mixin,从而造成命名冲突,使代码难以维护和理解。复杂性增加:随着mixin的引入,组件的复杂性呈指数增长。混合了多个mixin的组件往往难以追踪和调试,增加了代码维护的困难度。难以追踪数据流:组件的状态和逻辑被分散在多个mixin中,使得数据流难以追踪和理解。这增加了排查错误和进行性能优化的难度。组件间耦合:由于mixin的引入,组件之间的耦合性增强。一个组件可能会依赖于其他组件中定义的mixin,导致组件之间的依赖关系错综复杂。继承链问题:mixin使用继承链来将逻辑注入到组件中,但这会导致不可预测的继承链问题,特别是在复杂的项目中。总体而言,mixin的弊端主要表现在引入了难以管理的复杂性、命名冲突、耦合性增强等方面,因此React官方明确表示不建议使用mixin,而推荐采用更灵活、可维护的Hooks模式。Hooks提供了更清晰、可组合的方式来处理组件的状态和逻辑,避免了mixin带来的诸多问题。React官方在提供HooksAPI后,并没有强制要求开发者立刻转向使用它,而是通过明确Hooks的优势与劣势,让开发者自行选择。这种渐进的改变让项目中的开发者可以同时使用熟悉的Class组件和尝试新颖的Hooks。随着项目的逐步迭代,开发者在实践中逐渐体会到Hooks的优势。这种悄无声息的变革使越来越多的开发者熟悉并纷纷加入Hooks的行列。二、实战演练主要演示v16提供的10种和v18中提供的5种ReactHooksAPI的使用1.useStateuseState:定义变量,使其具备类组件的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 (          数字:{count}       setCount(count + 1)}>        第一种方式+1             setCount((v) => v + 1)}      >        第二种方式+1            );};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 (          数字形式:{count}       {          setCount(count+1);        }}      >        点击+1            对象形式:{state.number}       {          state.number++;          setState(state);        }}      >        点击+1            );};export default Index;2.useEffectuseEffect:副作用,这个钩子成功弥补了函数式组件没有生命周期的缺陷,是我们最常用的钩子之一。基本使用:useEffect(()=>{     return destory}, deps)Params:callback:useEffect的第一个入参,最终返回destory,它会在下一次callback执行之前调用,其作用是清除上次的callback产生的副作用;deps:依赖项,可选参数,是一个数组,可以有多个依赖项,通过依赖去改变,执行上一次的callback返回的destory和新的effect第一个参数callback。案例:模拟挂载和卸载阶段:事实上,destory会用在组件卸载阶段上,把它当作组件卸载时执行的方法就ok,通常用于监听addEventListener和removeEventListener上,如:import { useState, useEffect } from "react";import { Button } from "antd";const Child = () => {  useEffect(() => {    console.log("挂载");    return () => {      console.log("卸载");    };  }, []);  return react hooks!;};const Index = () => {  const [flag, setFlag] = useState(false);  return (           {          setFlag((v) => !v);        }}      >        {flag ? "卸载" : "挂载"}            {flag & }      );};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 (                  number: {number} count: {count}             setNumber((v) => v + 1)}>        number + 1             setCount((v) => v + 1)}      >        count + 1            );};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 (           setCount((v) => v + 1)}>        数字加一:{count}             setFlag((v) => !v)}      >        状态切换:{JSON.stringify(flag)}            );};export default Index;3.useContextuseContext:上下文,类似于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 (          父组件中的count:{count}       setCount((v) => v + 1)}>        点击+1                                );};const Child = () => {  const countChild = useContext(CountContext);  return (          子组件获取到的count: {countChild}            );};const Son = () => {  const countSon = useContext(CountContext);  return 孙组件获取到的count: {countSon};};export default Index;4.useReduceruseReducer:功能类似于redux,与redux最大的不同点在于它是单个组件的状态管理,组件通讯还是要通过props。简单地说,useReducer相当于是useState的升级版,用来处理复杂的state变化。基本使用:const [state, dispatch] = useReducer(    (state, action) => {},     initialArg,    init)arams: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 (          count:{count}       dispatch({ type: "add", payload: 1 })}      >        加1             dispatch({ type: "sub", payload: 1 })}      >        减1            );};export default Index;特别注意:在reducer中,如果返回的state和之前的state值相同,那么组件将不会更新。比如这个组件是子组件,并不是组件本身,然后我们对上面的例子稍加更改,看看这个问题:const Index = () => {  console.log("父组件发生更新");  ...  return (            ...       dispatch({ type: "no", payload: 1 })}      >        无关按钮                  )};const Child = ({ count }) => {  console.log("子组件发生更新");  return 在子组件的count:{count};};可以看到,当count无变化时,子组件并不会更新。5.useMemo场景:在每一次的状态更新中,都会让组件重新绘制,而重新绘制必然会带来不必要的性能开销,为了防止没有意义的性能开销,ReactHooks提供了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 (          数字集合:{JSON.stringify(data)}       setFlag((v) => !v)}>        状态切换{JSON.stringify(flag)}            );};export default Index;从例子中来看,按钮切换的flag应该与usePow的数据毫无关系,可以看到,当我们点击按钮后,会打印我是usePow,这样就会产生开销。毫无疑问,这种开销并不是我们想要见到的结果,所以有了useMemo。并用它进行如下改造:const usePow = (list) => {  return useMemo(    () =>      list.map((item) => {        console.log(1);        return Math.pow(item, 2);      }),    []  );};6.useCallbackuseCallback:与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 (           setCount((v) => v + 1)}>普通点击      useCallback点击      数字:{count}       setFlag((v) => !v)}>        切换{JSON.stringify(flag)}            );};const TestButton = memo(({ children, onClick = () => {} }) => {  console.log(children);  return (          {children}      );});export default Index;简要说明下,TestButton里是个按钮,分别存放着有无useCallback包裹的函数,在父组件Index中有一个flag变量,这个变量同样与count无关,那么,我们切换按钮的时候,TestButton会怎样执行呢?可以看到,我们切换flag的时候,没有经过useCallback的函数会再次执行,而包裹的函数并没有执行(点击“普通点击”按钮的时候,useCallbak的依赖项count发生了改变,所以会打印出useCallback点击)。7.useRefuseRef:用于获取当前元素的所有属性,除此之外,还有一个高级用法:缓存数据。基本使用:const ref = useRef(initialValue)arams: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 (                   可视区域高度:{clientHeight}         滚动条滚动高度:{scrollTop}         滚动内容高度:{scrollHeight}                                );};export default Index;8.useImperativeHandleuseImperativeHandle:可以通过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      点击次数:{count}     add()}> 子组件的按钮,点击+1  }const Index = () => {  const ref = useRef
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 12:44 , Processed in 0.341313 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表