react 里面的 useEffectEvent 的执行原理是怎样的?

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  const onMove = useEffectEvent(e => {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  });

  useEffect(() => {
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        The dot is allowed to move
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

useEffect 的依赖项使用了 [],那么内部的 effect 函数只会在初始渲染时执行,那么监听的 onMove 函数应该第一次组件函数执行时创建的 onMove,如果把 useEffectEvent 去掉,那么 canMove 也应该是第一次渲染时使用的 canMove,之后切换 canMove 是不会改变 canMove 的值的。如果使用了 useEffectEvent, 那么 canMove 会跟着切换的 canMove 变化的,useEffectEvent 是怎么实现总是读取到最新的状态值的?

阅读 1.6k
avatarAI BotBETA

在 React 中,useEffect 是一个内建的 Hook,用于在函数组件中执行副作用操作。然而,useEffectEvent 并不是 React 的官方 API。你提到的 useEffectEvent 可能是某个库或实验性功能的一部分,或者是自定义的 Hook。

在标准的 React 中,useEffect 的行为是这样的:它会接收一个函数和一个依赖项数组作为参数。当组件渲染时,这个函数会被调用,并且它的依赖项会被用于决定是否再次执行该函数。如果依赖项数组中的任何一个值发生变化,函数就会被再次调用。如果依赖项数组为空([]),则函数只会在组件首次渲染时执行一次。

在你的示例中,useEffect 的依赖项数组为空,因此它只会在组件首次渲染时执行一次,添加 pointermove 事件监听器,并且使用当时的 onMove 函数作为回调。由于 onMove 函数在首次渲染时创建,并且 canMove 状态在后续渲染中不会改变 onMove 函数,因此 onMove 函数内部使用的 canMove 值将保持在组件首次渲染时的值。

要实现类似于你描述的行为,使得 canMove 总是保持最新值,你需要让 onMove 函数在每次 canMove 变化时重新创建。这可以通过将 onMove 函数放在 useEffect 的依赖项数组中来实现,或者使用 useCallback Hook 来确保函数引用在依赖项变化时更新。

例如,你可以这样修改代码:

import { useState, useEffect, useCallback } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  const onMove = useCallback(e => {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  }, [canMove]); // 添加 canMove 作为依赖项

  useEffect(() => {
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, [onMove]); // 添加 onMove 作为依赖项

  // ... 其余代码保持不变
}

这样,每当 canMove 状态变化时,onMove 函数都会重新创建,并且包含最新的 canMove 值。因此,无论何时触发 pointermove 事件,onMove 函数都会使用最新的 canMove 值。

2 个回答
✓ 已被采纳

你可以理解为保证函数引用,但是函数内部状态依旧更新,类似这段伪代码

function useEffectEvent(handler) {
  const handlerRef = useRef(null);
  useLayoutEffect(() => {
    handlerRef.current = handler;
  });

  return useCallback((...args) => {
    const fn = handlerRef.current;
    return fn(...args);
  }, []);
}

这里就看你对 ref 的理解深不深了,ref 本质上就是一块共享内存

基于共享内存的思想,用 useRef 存储最新的函数 + useCallback 返回包装函数的固定引用就能实现

在包装函数中调用 ref 中的新函数即可

import { useCallback, useRef } from 'react';

export default function useEvent<T extends Function>(callback: T) {
    const ref = useRef<T>();

    ref.current = callback;

    return useCallback((...args) => {
        return ref.current?.(...args);
    }, []);
}
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题