通过造组件来学习React-2

前言

本文继续上一篇的内容。要实现:点击菜单框以外的其他区域,就会收起列表。

事件

那么问题来了,怎么知道点击的区域是不是菜单框以外的区域?

我想你已经知道,当点击一个元素时会触发这个元素所绑定的点击事件。

const btn = document.getElementById("btn");
btn.addEventListener("click", function (event) {
  //  点击事件
});

通过 addEventListener 绑定点击事件,事件通过方法的形式传入,该方法可以接收一个 event 参数,event 参数表示事件触发时的元素。参考:EventTarget.addEventListener()

简单说明一下这个 event 参数,如下代码:

<style>
  div {
    width: 200px;
    height: 200px;
    background-color: gray;
  }
</style>

<div id="out">
  <button>BTN</button>
</div>

<script>
  const out = document.getElementById("out");
  out.addEventListener(
    "click",
    function (event) {
      console.log(event.target);
      console.log(event.currentTarget);
    },
    false
  );
</script>

界面如下:

image.png

div 标签内有一个 按钮,同时 div 元素绑定了一个事件,该事件会打印出 event 的 target 属性和 currentTarget 属性。

当我们点击按钮后,由于按钮没有绑定点击事件,所以不会有任何事发生,然后事件冒泡到 div 元素上,触发了 div 的点击事件,控制台输出如下:

image.png

可见,target 属性输出的是 触发事件的按钮元素,currentTarget 属性输出的是绑定事件的 div 元素。所以,我们可以通过 event.target 得知触发事件的是什么元素。参考:Event

既然如此,我们的思路就是,给整个 window 绑定点击事件,如果事件的 event.target 不是菜单框中的元素,就将菜单框隐藏。

window.addEventListener("pointerdown", callback);

这里使用的 pointerdown 事件,是因为触摸屏的手指点击,画板的画笔也可以触发,兼容性好。参考:Pointer events 指针事件

假设菜单框的按钮元素和列表元素本别是:button 和 list,我们怎么知道有没有点击到他呢。只需要知道 event.target 在不在 button 和 list 这两个元素的范围内即可。只需要使用 Node 节点的方法 .contains(..) 即可判断传入的元素是不是子元素和当前元素。如,判断 event.target 是不是 button 的子元素和当前元素,只需要:

if (button.contains(event.target)) {
  //  ...
}

参考:Node.contains

这样一来,我们的思路就很明显了,使用 .contains() 判断 event.target,如果为 true,说明点击到的是按钮或列表,什么事都不做,如果不是,就隐藏列表。

if (button.contains(event.target)) {
  //  别忘了上一篇中的 dipatch
  dipatch({ type: "close" });
}
if (list.contains(event.target)) {
  dipatch({ type: "close" });
}

封装一下

在上一篇中,我们在 state 中存了按钮和列表的引用:

type StateDefine = {
  visiable: Boolean,
  buttonRef: React.MutableRefObject<HTMLButtonElement | null>,
  itemsRef: React.MutableRefObject<HTMLSpanElement | null>,
};

所以当判断的时候,要取出引用里的元素:

if (state.buttonRef?.current?.contains(event.target)) {
  //  别忘了上一篇中的 dipatch
  dipatch({ type: "close" });
}

组合并封装起来就成为了:

type Containers = Contaiter[];
type Contaiter = HTMLElement | React.MutableRefObject<HTMLElement | null>;

export function useOutsideClick(
  containers: Containers,
  event: (e: PointerEvent) => void,
  options?: any
) {
  const callback = React.useCallback(
    (e: PointerEvent) => {
      const target = e.target as HTMLElement;
      if (
        containers.some((element: Contaiter) => {
          const node =
            element instanceof HTMLElement ? element : element?.current;
          return node?.contains(target);
        })
      ) {
        return;
      }
      event(e);
    },
    [event]
  );

  React.useEffect(() => {
    console.log("ADD LISTENER");
    window.addEventListener("pointerdown", callback, options);
    return () => {
      console.log("REMOVE LISTENER");
      window.removeEventListener("pointerdown", callback, options);
    };
  }, []);
}

突然封装成了一个不认识的函数,我慢慢来解释。

我把他封装了一个名为 useOutsideClick 的 hook,用于判断点击元素是不是指定的元素,如果是就执行事件。能够看到接收的三个参数,containers 用于指定哪些元素,event 用于指定要执行的事件,options 是一个可选的参数。

然后你会看到一个 useCallback 方法,这个是 React 提供的 hook,作用是缓存一个方法,作为第一个参数传入。该方法会返回一个方法,除非第二个参数里的依赖项更新,否则会一直返回缓存的方法。参考:Hook API 索引 useCallback

紧接着我使用了 useEffect hook,useEffect 接收一个方法和一个依赖项,方法会在组件加载完成后执行,如果有依赖项的话,那么只有在组件首次加载和依赖项变动后才会执行方法。参考:Hook API 索引 useEffect。使用这个 hook,我们在菜单框加载完毕后给 window 添加事件,然后在菜单框卸载后删除事件。我分别在加载和卸载方法中输出了日志,以查看效果。

但如果你使用的是 React 18,且使用了 <React.StrictMode></React.StrictMode> 的话,你可能会见到控制台输出了如下信息:

ADD LISTENER
REMOVE LISTENER
ADD LISTENER

这是 React 18 使用了 <React.StrictMode></React.StrictMode> 在开发模式下使用 useEffect 后的怪异行为,useEffect 会执行两次,目的是为了让开发者检查 useEffect 有没有忘记添加卸载的函数。这种 feature 很是难受。参考:React18 的 useEffect 新特性为什么被疯狂吐槽?。让 useEffect 回归正常只要把 <React.StrictMode></React.StrictMode> 去掉就可以了。

当使用的时候,只需要:

useOutsideClick([state.buttonRef, state.itemsRef], (e) => {
  dispatch({ type: "close" });
});

试一下吧。

参考

EventTarget.addEventListener() by MDN

Event by MDN

Pointer events 指针事件 by MDN

Node.contains by MDN

Hook API 索引 useCallback by React

Hook API 索引 useEffect by React

React18 的 useEffect 新特性为什么被疯狂吐槽? by YvetteLau

posted @ 2022-06-21 14:07  DvorakChen  阅读(60)  评论(0)    收藏  举报