【ahooks源码解读06】最新值与函数持久化之useLatest与useMemoizedFn

各位读者,又见面了,欢迎!

如果我写的东西,对大家有那么一点帮助,希望大家点个关注和收藏,如果有说的不对的地方,也欢迎大家在评论区喷我

"新"与"旧"

JS技术栈中,值是不是最新的其实困扰着很多的初学者。

很多时候我们想始终拿到最新的值,但是由于闭包的作用域问题可能拿到的是"旧"的值

比如还是拿我们源码解读02的例子来说,

jsx 复制代码
import { useEffect, useState } from "react";

const useUnmount = (fn) => {
  useEffect(
    () => () => {
      fn?.(); 
    },
    [],
  );
};
const Child = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  useUnmount(() => {
    console.log("count:", count);
  });
  return <div>{count}</div>;
};

export default function Test() {
  const [flag, setFlag] = useState(false);

  return (
    <div>
      <button
        onClick={() => {
          setFlag(!flag);
        }}
      >
        点我
      </button>
      {!!flag ? <Child /> : null}
    </div>
  );
}

这里面要改是存在两处的问题的。

  1. useUnmount的实现中传入的函数不能取到最新的count
  2. interval中每次累加的时候本身count值始终是从0开始的,所以页面也始终是1

如何修改,大家可以链接到原文去看下

因此我们这篇就来介绍下ahooks源码实现和我们使用ahooks时,关于useLatestuseMemoizedFn的一些知识点

"新" - useLatest

源码实现

ts 复制代码
import { useRef } from 'react';

function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;

  return ref;
}

export default useLatest;

解读

useLatest的实现非常简单。它使用了useRef来保存传入的value。每次组件渲染时,ref.current都会被更新为最新的value。这样,无论什么时候访问ref.current,都能获取到最新的value

应用场景:

  1. 避免闭包问题 :在某些情况下,我们可能会在回调函数中使用到组件的某些状态值,如果不使用useLatest,这些状态值可能会因为闭包的特性而无法获取到最新的值。例如,在事件处理函数中使用setIntervalsetTimeout时,使用useLatest可以确保访问到最新的状态。
jsx 复制代码
import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';

export default () => {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  const latestCountRef = useLatest(count);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(latestCountRef.current + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount2(count2 + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <>
      <p>count(useLatest): {count}</p>
      <p>count(defult): {count2}</p>
    </>
  );
};

有同学肯定要问了,为什么使用useRef就可以了

On the next renders, useRef will return the same object.

来自React官网的描述,翻译过来的意思就是在后续的渲染时,始终给你返回相同的对象,对象的值引用不变,我们在需要的时候去修改或者读取它的current,就始终得到了最新值。

PS:React中,useRefcurrent的修改不会导致React组件的重新渲染

"旧"- useMemoizedFn

提醒大家注意一下 这里的"旧"其实是一个缓存持久化的感念

源码实现

ts 复制代码
import { useMemo, useRef } from 'react';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';

type noop = (this: any, ...args: any[]) => any;

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>;

function useMemoizedFn<T extends noop>(fn: T) {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`);
    }
  }

  const fnRef = useRef<T>(fn);

  // why not write `fnRef.current = fn`?
  // https://github.com/alibaba/hooks/issues/728
  fnRef.current = useMemo<T>(() => fn, [fn]);

  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
}

export default useMemoizedFn;

解读

useMemoizedFn同样是利用useRef来保存传入的函数fn。每次组件渲染时,fnRef.current都会被更新为最新的fn。通过useCallback返回一个记忆化的函数memoizedFn,这个函数在执行时总是调用最新的fnRef.current

应用场景:

  1. 保持函数引用的一致性:在传递函数给子组件时,保持函数引用的一致性可以避免子组件的额外渲染。
jsx 复制代码
import React from 'react';
import useMemoizedFn from './useMemoizedFn';

function ParentComponent() {
  const handleAction = useMemoizedFn(() => {
    console.log('Action handled');
  });

  return <ChildComponent onAction={handleAction} />;
}

function ChildComponent({ onAction }) {
  return <button onClick={onAction}>Click me</button>;
}

export default ParentComponent;
}
  1. 依赖稳定的回调函数 :在某些情况下,我们需要将一个回调函数传递给第三方库,这些库可能依赖回调函数的引用一致性。使用useMemoizedFn可以确保回调函数的引用在整个组件生命周期内保持稳定。

这里我们看下官网demo2

jsx 复制代码
import { useMemoizedFn } from 'ahooks';
import { message } from 'antd';
import React, { useCallback, useRef, useState } from 'react';

export default () => {
  const [count, setCount] = useState(0);

  const callbackFn = useCallback(() => {
    message.info(`Current count is ${count}`);
  }, [count]);

  const memoizedFn = useMemoizedFn(() => {
    message.info(`Current count is ${count}`);
  });

  return (
    <>
      <p>count: {count}</p>
      <button
        type="button"
        onClick={() => {
          setCount((c) => c + 1);
        }}
      >
        Add Count
      </button>

      <p>You can click the button to see the number of sub-component renderings</p>

      <div style={{ marginTop: 32 }}>
        <h3>Component with useCallback function:</h3>
        {/* use callback function, ExpensiveTree component will re-render on state change */}
        <ExpensiveTree showCount={callbackFn} />
      </div>

      <div style={{ marginTop: 32 }}>
        <h3>Component with useMemoizedFn function:</h3>
        {/* use memoized function, ExpensiveTree component will only render once */}
        <ExpensiveTree showCount={memoizedFn} />
      </div>
    </>
  );
};

// some expensive component with React.memo
const ExpensiveTree = React.memo<{ [key: string]: any }>(({ showCount }) => {
  const renderCountRef = useRef(0);
  renderCountRef.current += 1;

  return (
    <div>
      <p>Render Count: {renderCountRef.current}</p>
      <button type="button" onClick={showCount}>
        showParentCount
      </button>
    </div>
  );
});

当我们点击add count的时候,示例中 memoizedFn 是不会变化的,callbackFn 在 count 变化时变化。

总结:在React开发中掌握不变性的艺术与引用的智慧

由于这两个Hook在ahooks内部实现中被反复复用,对业务开发来说也是非常高频的工具hook,因此我们这期重点拿出来讲了下。

React开发中,我们常常面对状态管理、渲染优化和闭包陷阱等复杂问题。而利用useRefuseLatestuseMemoizedFn等hooks,我们可以更好地管理这些问题,从而编写出高效、稳定和易维护的代码。

不变性与引用的智慧

  1. 不变性

    • 不可变数据结构:在React中,状态更新的最佳实践是使用不可变数据结构。这不仅符合React通过浅比较进行高效重渲染的机制,还能避免因为直接修改对象或数组而引发的潜在问题。
    • 状态更新:每次状态更新时,创建新的对象或数组,而不是直接修改原有的。这确保了React能够检测到状态的变化,从而正确触发重新渲染。
  2. 引用的智慧

    • useRefuseRef提供了一种方法,可以在组件的整个生命周期内持久化数据,而不触发组件的重新渲染。它在处理非状态数据的持久化存储方面非常有效,如保持某个值在多次渲染之间的一致性。
    • useLatest :在处理闭包问题时,useLatest确保了在回调函数中总是可以访问到最新的状态或属性值。这在处理定时器、事件监听器等异步操作时尤其重要。
    • useMemoizedFn :通过useMemoizedFn,可以确保函数引用在整个组件生命周期内保持一致,避免因函数引用变化而导致的子组件不必要的重新渲染。

通过理解和利用这些特性,我们可以编写出更加高效和健壮的React组件,提升应用的性能和用户体验。

相关推荐
请叫我欧皇i18 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_20 分钟前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
guokanglun26 分钟前
空间数据存储格式GeoJSON
前端
zhang-zan1 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
猫爪笔记1 小时前
前端:HTML (学习笔记)【2】
前端·笔记·学习·html
brief of gali1 小时前
记录一个奇怪的前端布局现象
前端
Json_181790144802 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网3 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子02043 小时前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing3 小时前
React核心功能详解(一)
前端·react.js·前端框架