|
在当今的前端开发环境中,越来越多的开发者认可了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
|
|