React useCreation hook 思考与解析

514 阅读4分钟

React useCreation hook 思考与解析

本文主要对于掘金小册《玩转 React Hooks》的思考

引文

以下内容摘自《玩转 React Hooks》

useCreation:强化 useMemo 和 useRef,用法与 useMemo 一样,一般用于性能优化。

useCreation 如何增强:

  • useMemo 的第一个参数 fn,会缓存对应的值,那么这个值就有可能拿不到最新的值,而 useCreation 拿到的值永远都是最新值
  • useRef 在创建复杂常量的时候,会出现潜在的性能隐患(如:实例化 new Subject),但 useCreation 可以有效地避免。

思考

useMemo无法获取最新值得情况

useMemo是一个React Hook,它可以在渲染期间优化组件的性能。useMemo的作用是在函数组件中缓存计算结果,只有在依赖项发生变化时,才会重新计算结果。如果依赖项没有发生变化,useMemo将会直接返回上一次的计算结果,不会重新计算。

所以,在以下情况下,useMemo可能无法返回最新的值:

  1. 当依赖项没有发生变化时,useMemo会返回上一次缓存的值。因此,如果你需要访问最新的状态,请确保组件的依赖项已经发生了变化。
  2. 如果你使用了异步操作或副作用操作,并且这些操作不是在useMemo中管理的,那么useMemo的值可能无法保证最新。
  3. 如果你修改了组件依赖项之外的其他部分,即使依赖项发生了变化,useMemo 也可能无法返回最新的值。

异步操作的影响

下面是一个简单的例子,模拟了一个通过网络请求获取数据的情况:

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

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('data');
    }, 5000);
  });
}

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const expensiveValue = useMemo(async () => {
    const data = await fetchData();
    return `${data}-${count}`;
  }, [count]);
  console.log('expensiveValue:', expensiveValue);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

export default ExpensiveComponent;

async关键字声明的函数,返回数据的类型是Promise,那么我们不能直接将值显示在页面上,所以直接将结果打印出来

在这个例子中,我们模拟了一个通过网络请求获取数据的情况,useMemo的依赖项是count。当你点击“Increase Count”按钮时,计数器会增加一,这意味着useMemo的依赖项发生了变化,并且useMemo应该返回一个新的值。

image.png 你会发现useMemo返回的值Promise的状态都是pending,无法获取到最新的值

这是因为,网络请求是在useMemo内部之外发生的,useMemo无法控制,因此,即使依赖项发生了变化,useMemo也会返回上一次缓存的值。

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

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);
  const expensiveValue = useMemo(() => {
    if (!data) return null;
    return `${data}-${count}`;
  }, [data, count]);

  useMemo(async () => {
    const result = await fetchData();
    setData(result);
  }, []);

  console.log('expensiveValue:', expensiveValue);

  return (
    <div>
      <p>{expensiveValue}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

解析

源码

import { useRef, type DependencyList } from "react";

/**
 * 判断依赖项是否相同
 * @param oldDeps 旧的依赖项
 * @param deps 新的依赖项
 * @returns 
 */
const depsAreSame = (
    oldDeps: DependencyList,
    deps: DependencyList
) => {
    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,
    })
    /**
     * 触发更新的两种情况
     * 1. 未初始化
     * 2. 依赖项的改变
     */
    if (!current.initialized || !depsAreSame(current.deps, deps)) {
        current.deps = deps
        current.initialized = true
        current.obj = fn()
        console.log(current.obj)
    }

    return current.obj as T
}

export default useCreation


export default ExpensiveComponent;

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('data');
    }, 1000);
  });
}

在这个新的代码中,我们把网络请求的部分放到单独的useMemo中,并且在依赖项中传入一个空数组,表示这部分代码只需要执行一次,这其实效果和useEffect(() => {....},[])相同。并且在useMemo的依赖项中添加data状态,useMemo就可以管理这部分代码了,并且可以保证expensiveValue始终是最新的值。

总结

假设你的组件通过useMemo计算一个比较耗时的值,并且这个值需要通过网络请求来获取,这时候就需要用到异步操作。但是,如果你的网络请求不是在useMemo内部进行管理,useMemo返回的值可能就无法保证最新。

解析

import { useRef, type DependencyList } from "react";

/**
 * 判断依赖项是否相同
 * @param oldDeps 旧的依赖项
 * @param deps 新的依赖项
 * @returns 
 */
const depsAreSame = (
    oldDeps: DependencyList,
    deps: DependencyList
) => {
    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,
    })
    /**
     * 触发更新的两种情况
     * 1. 未初始化
     * 2. 依赖项的改变
     */
    if (!current.initialized || !depsAreSame(current.deps, deps)) {
        current.deps = deps
        current.initialized = true
        current.obj = fn()
    }

    return current.obj as T
}

export default useCreation

useRef存储hook的deps(依赖数组)、initialized(是否初始化)、obj(存储的变量),这个参数的作用是应对首次保存值,之后判断是否保存,根据 deps 判断即可。