⚫ React Hooks 精进 4 - 内置Hooks(二)回调函数


useCallback,useMemo,useRef,useContext

useCallback:缓存回调函数

函数组件中并没有一个直接的方式在多次渲染之间维持一个状态,所以一些定义在函数组件内的函数在多次渲染之间无法重用,每次都需要创建一个新的。这样也会导致每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。

思路:只有当函数参数或调用变量发生变化时,才需要重新定义一个回调函数。

于是就有了 useCallback 这个 Hook。

useCallback(fn, deps); // fn: 回调函数, deps: 依赖项
const handleIncrement = useCallback(() => setCount(count + 1), [count]);

可以减少不必要的渲染,主要体现在将回调函数作为属性传给某个组件,因为如果函数被重新创建,那么组件的 props 就会发生变化也会导致子组件的重新渲染。

useMemo 也是为了缓存而设计的,useCallback 缓存的是一个函数,而 useMemo 缓存的是计算结果。

useMemo:缓存计算结果

useMemo(fn, deps); // fn: 计算函数, deps: 依赖项

如果某个数据是通过其他数据计算得到,那么只有当用到的变量(依赖项)发生变化时才会重新计算。

import React, { useState, useMemo, useEffect } from "react";

export default function SearchUserList() {
  const [users, setUsers] = useState(null);
  const [searchKey, setSearchKey] = useState("");

  useEffect(() => {
    const doFetch = async () => {
      // 组件首次加载时发请求获取用户数据
      const res = await fetch("https://reqres.in/api/users/");
      setUsers(await res.json());
    };
    doFetch();
  }, []);

  const usersToShow = useMemo(() => {
    if (!users) return null;
    return users.data.filter((user) => user.first_name.includes(searchKey));
  }, [users, searchKey]);

  return (
    <div>
      <input
        type="text"
        value={searchKey}
        onChange={(evt) => setSearchKey(evt.target.value)}
      />
      <ul>
        {usersToShow &&
          usersToShow.length > 0 &&
          usersToShow.map((user) => {
            return <li key={user.id}>{user.first_name}</li>;
          })}
      </ul>
    </div>
  );
}

可以避免一些重复计算,还可以避免子组件的重复渲染

♦️ 其实 useCallback 的功能其实可以用 useMemo 来实现。

useRef:在多次渲染之间共享数据

const refCon = useRef(initialValue);

可以把 useRef 看作是在函数组件之外创建的一个容器空间,在这个容器上,这样就可以通过唯一的 current 属设置的一个值,从而在函数组件的多次渲染之间共享这个值。

import React, { useState, useCallback, useRef } from "react";

export default function Timer() {
  // 定义 time state 用于保存计时的累积时间
  const [time, setTime] = useState(0);

  // 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
  const timer = useRef(null);

  // 开始计时的事件处理函数
  const handleStart = useCallback(() => {
    // 使用 current 属性设置 ref 的值
    timer.current = window.setInterval(() => {
      setTime((time) => time + 1);
    }, 100);
  }, []);

  // 暂停计时的事件处理函数
  const handlePause = useCallback(() => {
    // 使用 clearInterval 来停止计时
    window.clearInterval(timer.current);
    timer.current = null;
  }, []);

  return (
    <div>
      {time / 10} seconds.
      <button onClick={handleStart}>Start</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  );
}

使用 useRef 保存的数据一般是和 UI 渲染无关的,因此当 ref 的值发生变化时是不会触发组件的重新渲染的。

保存某个 DOM 节点的引用

就可以利用这个保存功能,就可以在渲染界面上访问到真实的 DOM 节点。

useContext:定义全局状态

能够让所有组件在某个组件开始的组件树上创建一个 Context,这样这个组件树上的所有组件都能访问和修改这个 Context。

const context = React.createContext(initialValue);
const value = useContext(context);

context 具有一个 Provider 的属性,一般作为组件树的根组件。

import React, { useState, useContext, useCallback } from "react";

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee",
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222",
  },
};

// 创建一个 Theme 的 Context
const ThemeContext = React.createContext(themes.light);

// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button
      style={{
        background: theme.background,
        color: theme.foreground,
      }}
    >
      I am styled by theme context!
    </button>
  );
}

// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
export default function () {
  // 使用 state 来保存 theme 从而可以动态修改
  const [theme, setTheme] = useState("light");

  // 切换 theme 的回调函数
  const toggleTheme = useCallback(() => {
    setTheme((theme) => (theme === "light" ? "dark" : "light"));
  }, []);

  return (
    // 使用 theme state 作为当前 Context
    <ThemeContext.Provider value={themes[theme]}>
      <button onClick={toggleTheme}>Toggle Theme</button>

      <Toolbar />
    </ThemeContext.Provider>
  );
}

♦️ 缺点

  • 让调试变得困难
  • 让组件复用变得困难

思考:

useState 也能够在组件的多次渲染之间共享数据,那个那个 timer 计时器是否能用 state 去保存 window.setInterval()?

  • 可以但没必要,用 useState 保存的话会触发组件的重新渲染?
  • useRef 结果的改变不会触发重新渲染

文章作者: 阿汪同学
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 阿汪同学 !
评论
  目录