为什么我不能在我的react组件中删除事件监听器?

时间:2017-12-16 14:00:25

标签: reactjs typescript javascript-events

虽然我知道,这个问题已被多次询问,但通常的答案是"你传递的是副本,而不是原来的#34; 。除非我理解错误,否则这不是我的问题。

我使用以下代码在React中创建了一个模态:

const Modal: React.StatelessComponent<Props> = 
({ isOpen, closeModal, closeOnClick = true, style, children }: Props) => {

  const closeOnEsc: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
    e.stopImmediatePropagation();
    if (e.keyCode === 27) {
      closeModal();
    }
  };

  if (isOpen) {
    document.body.classList.add('freezeScroll');
    window.addEventListener('keyup', closeOnEsc, { once: true });
    return (
      <div className={styles.modal} onClick={closeOnClick ? closeModal : undefined} >
        <div className={`${styles.modalContent} ${closeOnClick ? styles.clickable : undefined} ${style}`} >
          {children}
        </div>
      </div>
    );
  }
  document.body.classList.remove('freezeScroll');
  window.removeEventListener('keyup', closeOnEsc, { once: true });
  return null;
};

我只添加了{once:true}选项,因为我无法将其删除。 关闭模态后,会调用window.removeEventListener,但它不会删除事件监听器。
任何人都可以帮我找出原因吗?

2 个答案:

答案 0 :(得分:0)

我不会亲自手动绑定SFC中的侦听器。如果你真的想要,我只是让回调删除听众。

closeOnEsc回调中,添加window.removeEventListener('keyup', closeOnEsc);

这个问题,如果在没有调用回调的情况下卸载组件,那么你的内存泄漏很少。

我会使用类组件,并确保在componentWillUnmount生命周期钩子中也删除了侦听器。

答案 1 :(得分:0)

您不应直接在渲染中附加事件侦听器。这些不是幂等的副作用,它会导致内存泄漏。在每次渲染时(即使它没有提交给 DOM),您都在分配事件侦听器在您的模态关闭后,您只删除最后一个。正确的做法是使用 useEffect(或 useLayoutEffect 钩子),或者将您的组件转换为类并使用生命周期方法。

这是带有钩子的带注释的示例:

const Modal: React.StatelessComponent<Props> = ({
  isOpen,
  closeModal,
  closeOnClick = true,
  style,
  children,
}: Props) => {
  useEffect(
    function () {
      // we don't need to add listener when modal is closed
      if (!isOpen) return;
      // we are closuring function to properly remove event listener later
      const closeOnEsc: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
        e.stopImmediatePropagation();
        if (e.keyCode === 27) {
          closeModal();
        }
      };
      window.addEventListener("keyup", closeOnEsc);
      // We return cleanup function that will be executed either before effect will be fired again,
      // or when componen will be unmounted
      return () => {
        // since we closured function, we will definitely remove listener
        window.removeEventListener("keyup", closeOnEsc);
      };
    },
    [isOpen]
  );

  useEffect(
    function () {
      // it is very tricky with setting class on body, since we theoretically can have multiple open modals simultaneously
      // if one of this model will be closed, it will remove class from body.
      // This is not particularly good solution - it is better to have some centralized place to set this class.
      // This is why I moved it into separate effect
      if (isOpen) {
        document.body.classList.add("freezeScroll");
      } else {
        document.body.classList.remove("freezeScroll");
      }
      // just in case if modal would be unmounted, we reset body
      return () => {
        document.body.classList.remove("freezeScroll");
      };
    },
    [isOpen]
  );

  if (isOpen) {
    return (
      <div
        className={styles.modal}
        onClick={closeOnClick ? closeModal : undefined}
      >
        <div
          className={`${styles.modalContent} ${
            closeOnClick ? styles.clickable : undefined
          } ${style}`}
        >
          {children}
        </div>
      </div>
    );
  }
  return null;
};

再说一遍。非常重要的是,类和函数组件的渲染方法没有任何非幂等的副作用。这种情况下的幂等性意味着如果我们多次调用某个副作用,它不会改变结果。这就是您可以从渲染中调用 console.log 的原因 - 它是幂等的,至少从我们的角度来看是这样。与反应钩子相同。您可以(并且应该!)在每次渲染时调用它们,而不会弄乱 React 内部状态。将新的事件侦听器附加到 DOM 或窗口不是幂等的,因为在每次渲染时您都附加了新的事件侦听器而无需清除前一个。

从技术上讲,您可以先删除事件侦听器,然后在没有 useEffect 的情况下附加新的,但在这种情况下,您必须以某种方式保留渲染之间的侦听器身份(引用),或者立即从窗口中删除所有 keyup 事件.前者很难做到(您在每次渲染时都创建新函数),后者会干扰程序的其他部分。