我们知道React Hooks有useState设置变量,useEffect副作用,useRef来获取元素的所有属性,还有useMemo、useCallback来做性能优化,当然还有一个自定义Hooks,来创造出你所想要的Hooks

接下来我们开看看几个问题,问问自己,是否全部知道

  • Hooks的由来是什么?
  • useRef的高级用法是什么
  • useMemo 和useCallback 是怎么做优化的
  • 一个好的自定义Hooks该如何设计
  • 如果做一个不需要useState就可以直接修改属性并刷新树图的自定义hooks?
  • 如果做一个可以监听任何事件自定义hooks

本文将会以介绍自定义Hooks来解答上述问题,并结合 TS,ahooks中的钩子,以案列的形式去演示,本文过长,建议:点赞 + 收藏 哦~ 注:这里讲解的自定义钩子可能会和 ahooks上的略有不同,不会考虑过多的情况,如果用于项目,建议直接使用ahooks上的钩子~

如果有小伙伴不懂TS,可以看看我的这篇文章:一篇让你完全够用TS的指南 (opens new window)

大纲

# 自定义Hooks是什么?

?react-hooks是React16.8以后新增的钩子API,目的是增加代码的可复用性、逻辑性,最重要的是解决了函数式组件无状态的问题,这样即保留了函数式的简单,又解决了没有数据管理状态的缺陷

那么什么是自定义Hooks呢?

自定义hooks是在react-hooks基础上的一个扩张,可以根据业务、需求去制定相应的hooks,将常用的逻辑进行封装,从而具备复用性

# 如何设计一个自定义Hooks

hooks本质上是一个函数,而这个函数主要就是逻辑复用,我们首先要知道一件事,hooks的驱动条件是什么?

其实就是props的修改,useState、useReducer的使用是无状态组件更新的条件,从而驱动自定义hooks。

# 通用模式

自定义hooks的名称是以use开头,我们设计为

TIP

const [xxx, ...] = useXXX(参数一, 参数二...)

# 简单的例子:usePow

我们先写一个简单的小李子来了解下 自定义hooks

// usePow.ts
const Index = (list: number[]) => {
    return list.map(item: number => {
        console.log(1);
        return Math.pow(item, 2);
    })
}

export default Index;

// Index.tsx;
import { Button } from 'antd-mobile';
import React, { useState } from 'react';
import { usePow } from '@/components';

const Index: React.FC<any> = (props) => {
    const [flag, setFlag] = useState<boolean>(true);
    const data  = usePow([1,2,3]);

    return (
        <div>
            <div>数字: {JSON.stringify(data)}</div>
            <Button color="primary" onClick = {() => setFlag(v => !v)}>切换</Button>
            <div> 切换状态</div>
        </div>
    )
}
export default Index;

我们简单的写了个usePow,我们通过usePow给所传入的数字平方,用切换状态的按钮表示函数内部的状态,我们来看看次数的效果:

usePow

我们发现了一个问题,为什么点击切换按钮也会触发console.log(1)呢?

这样明显增加了性能开销,我们的理想状态肯定不希望做无关的渲染,所以我们做自定义hooks的时候一定要注意,需要减少性能开销,我们为组件加入useMemo试试

import { usememo } from 'react';

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

usePow使用usememo

发现此时就已经解决了这个问题,所以要非常注意一点,一个好用的自定义hooks,一定要配合useMemo、useCallback等API一起使用

# 玩转React Hooks

在上述中我们讲了用useMemo来处理无关的渲染,接下来我们一起来看看React Hooks的这些钩子的妙用(这里建议先熟知、并使用对应的React Hooks,才能造出好的钩子)

# useMemo

当一个父组件中调用了一个子组件的时候,父组件的state发生变化,会导致父组件更新,而子组件虽然没有发生变化,但也会进行更新。

简单的理解下,当一个页面内容非常复杂,模块非常多的时候,函数式组件会从头更新到尾,只要一处改变,所有的模块都会进行更新,这种情况是没有必要的

我们理想的状态是哥哥模块只进行自己的更新,不要想去去影响,那么此时用useMemo是最佳的解决方案。

这里要尤其注意一点,只要父组件的状态更新,无论有没有对子组件进行操作,子组件都会进行更新,useMemo就是为了防止这点出现的

在讲useMemo之前,我们先说说memo,memo的作用是结合了pureComponent纯函数和componentShouldUpdate功能,会对传入的props进行一次对比,然后根据第二个函数返回值进一步判断哪些Props需要更新(具体使用会在下文讲到)

useMemo与memo的理念上差不多,都是判断是否满足当前的限制条件来决定是否执行callback函数,而useMemo的第二个参数是一个数组,通过这个数组来判定是否更新回调函数

这种方式可以运用在『元素、组件、上下文中』,尤其是利用在数组中,先看一个例子

useMemo(() => {
    <div>
        {
            list.map((item, index) => {
                <p key={index}>
                    {item.name}
                </p>
            })
        }
    </div>
}, [list])

从上面我们看出useMemo只有在list发生变化的时候才会进行渲染,从而减少不必要的开销

总结一下useMemo的好处

  • 可以减少不必要的循环和不必要的渲染
  • 可以减少子组件的渲染次数
  • 可以特定的依赖进行更新,可以避免很多不必要的开销,但要注意,有时候在配合useState拿不到最新的值,这种情况可以考虑useRef解决

# useCallback

useCallback与useMemo极其类似,可以说是一模一样,唯一不同的是useMemo返回的是函数运行的结果,而useCallback返回的是函数

注意:这个函数是父组件传递子组件的一个函数,防止做无关的刷新,其次,这个组件必须配合memo,否则不但不会提升性能,还有可能降低性能

import React, { useState, useCallback } from 'react';
import { Button } from 'antd-mobile';

const MockMemo: React.FC<any> = () => {
    const [count, setCount] = useState(0);
    const [show, setShow] = useState(true);

    const add = useCallback(() => {
        setCount(count + 1);
    }, [count]);

    return (
        <div>
            <div style={{display: 'flex', jusifyContent: 'flex-start'}}>
                <TestButton title="普通点击" onClick={() => setCount(count + 1)}/>
                <TestButton title="useCallback点击" onClick={add} />
            </div>
            <div style={{marginTop: 20}}>count: {count}</div>
            <Button onClick={() => {setShow(!show)}}> 切换</Button>
        </div>
    )
}
const TestButton = React.memo((Props: any) => {
    console.log(props.title);
    return <Button color="primary" onClick={props.onClick} style={props.title === 'useCallback点击' ? {
        marginLeft: 20,
    } : undefined }>{props.title}</Button>
})

useCallback

可以看到,当点击切换按钮的时候,没有经过useCallback封装的函数会再次刷新,而经过useCallback包裹的函数不会被再次刷新。

# useRef

useRef可以获取当前元素的所有属性,并且返回一个可变的ref对象,并且这个对象只有current属性,可设置initialValue

# 通过useRef获取对应的属性值

import React, { useState, useRef } from 'react';

const Index:React.FC<any> = () => {
    const scrollRef = useRef<any>(null);
    const [clientHeight, setClientHeight] = useState<number>(0);
    const [scrollTop, setScrollTop] = useState<number>(0);
    const [scrollHeight, setScrollHeight] = useState<number>(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>
            <div>
                <p>可视区域高度:{clientHeight}</p>
                <p>滚动条滚动高度:{scrollTop}</p>
                <p>滚动内容高度:{scrollHeight}</p>
            </div>
            <div style={{height: 200, overflowY: 'auto'}} ref={scrollRef} onScroll={onScroll}>
                <div style={{height: 20000}}></div>
            </div>
        </div>
    )
}
export default Index;

从上述可知,我们可以通过useRef来获取对应元素的相关属性,从此来做一些操作

useRef

# 缓存数据

除了获取对应的属性外,useRef还有一点比较重要的特性,那就是缓存数据

上述讲到我们封装一个合格的自定义hooks的时候需要结合 useMemo、useCallback等Api,但我们控制变量的值用useState有可能会导致拿到的是旧值,并且如果他们更新会带来整个组件重新执行,这种情况下,我们使用useRef是非常不错的选择。

在react-redux的源码中,在hooks推出后,react-redux用大量的useMemo重做了Provide等核心模块,其中就是运用useRef来缓存数据,并且所运用的userRef()没有一个是绑定在dom上的,都是做数据缓存作用

简单来看一下

// 缓存数据
/* react-redux 用userRef 来缓存 merge之后的 props */ 
const lastChildProps = useRef() 

// lastWrapperProps 用 useRef 来存放组件真正的 props信息 
const lastWrapperProps = useRef(wrapperProps) 

//是否储存props是否处于正在更新状态 
const renderIsScheduled = useRef(false)

//更新数据
function captureWrapperProps( 
    lastWrapperProps, 
    lastChildProps, 
    renderIsScheduled, 
    wrapperProps, 
    actualChildProps, 
    childPropsFromStoreUpdate, 
    notifyNestedSubs 
) { 
    lastWrapperProps.current = wrapperProps 
    lastChildProps.current = actualChildProps 
    renderIsScheduled.current = false 
}

我们看到react-redux用重新赋值的方法,改变了缓存的数据源,减少了不必要的更新,如果采用useState势必会重新渲染

# useLatest

经过上面的讲解我们知道useRef可以拿到最新值,我们可以进行简单的封装,这样做的好处是:可以随时确保获取的是最新值,并且也可以解决闭包问题

import {useRef} from 'react';

const useLatest = <T>(value: T) => {
    const ref = useRef(value);
    ref.current = value;
    return ref;
}
export default useLatest;

# 结合useMemo和useRef封装useCreation

useCreation:是useMemo或useRef的替代品。换言之,useCreation这个钩子增强了useMemo 和 useRef,这个钩子替换这两个钩子(useCreation (opens new window))

  • useMemo的值不一定是最新的值,但useCreation可以保证拿到的值一定是最新的值
  • 对于复杂常量的创建,useRef容易出现潜在的性能隐患,但useCreation可以避免

这里的性能隐患是指

// 每次重渲染,都会执行实例化Subject的过程,即便这个示例立刻就被扔掉了
const a = useRef(new Subject());

// 通过factory函数 可以避免性能隐患
const b = useCreation(() => new Subject(), [])

接下来我们来看看如何封装一个useCreation,首先我们要明白一下三点:

  • 第一点:先确定参数,useCreation的参数与useMemo的一致,第一个参数是函数,第二个参数是可变的数组
  • 第二点: 我们的值要保存在useRef中,这个可以将值缓存,从而减少无关的刷新
  • 第三点: 更新值的判断,怎么通过第二个参数判断是否更新 useRef里的值

明白以上三点我们可以自己实现一个useCreation

import { useRef } from 'react';
import type { DependencyList } from 'react';

const depsAreSame = (oldDeps: DependencyList, deps: DependencyList): boolean => {
    if(oldDeps === deps) return true;

    for(let i = 0; i < oldDeps.length; i++) {
        // 判断两个值是否是同一个值
        if(!Object.is(oldDeps[i], deps[i)) return false;
    }
    return true;
}

const useCreation = <T>(fn: () => T, deps: DependencyList) => {
    const { current } = useRef({
        deps, 
        obj: undefined as undefined | T,
        initialized: false
    })
    if(current.initialized === false || !depsAreSame(current.deps, deps)) {
        current.deps = deps;
        current.obj = fn();
        current.initialized = true;
    }
    return current.obj as T;
}
export default useCreation;

在useRef判断是否更新通过initialized 和 depsAreSame 来判断,其中 depsAreSame 通过存储在useRef 下的 deps(旧值)和新传入的 deps(新值)来做对比,判断两数组的数组是否一致,来确定是否更新

# 验证 useCreation

# 资料

原文 (opens new window)