DOM – Event Listener (bubble, capture, passive, custom event)

前言

老掉牙的东西, 主要是想写 passive, 随便也写一点 bubble, capture 和 custom event 吧.

 

Bubble

Dom 监听事件是会冒泡的. 什么意思 ?

上图有 2 个 box, parent box 嵌套 child box. 假设 parent 和 child 都有事件监听.

const parentBox = document.querySelector(".parent-box")!;
const childBox = document.querySelector(".child-box")!;
parentBox.addEventListener('click', () => console.log('clicked'));
childBox.addEventListener('click', () => console.log('clicked'));

请问, 我现在点击 child box. parent box 的事件会触发吗? 

答案是会的. 因为事件会冒泡 (注: 它的意思不是你点击了 child 同时也点击了 parent 哦, 只是点击了 child, 然后 child 事件触发, 然后冒泡到了 parent)

冒泡可以被阻止. 哪个 child 被点击了也可以被判断出来

parentBox.addEventListener("click", (event) => {
  console.log(event.currentTarget); // the listening element : parent
  console.log(event.target); // the clicked element : could be child or parent
});

childBox.addEventListener("click", (event) => {
  event.stopPropagation(); // 阻止冒泡
  event.stopImmediatePropagation(); // 顺便讲一下这个, 它除了 stopPropagation 外, 当前 element 后续的 listener 也不会触发
});

假设 child stopPropagation 那么 parent 就不会触发了.

注1:

假设 child binding 了 2 个相同的 event(e.g. mousedown),只要其中一个 stop bubble 就够了。

如果 event 是不相同的,比如一个 mousedown 一个 mouseup,那么 stop bubble 只会影响有 stop buddle 的那一个。

注2:

有些 event 是不会 buddle 的,比如 mouseleave

 

Capture

首先事件触发的顺序是下面这样的

3 层 element. 其实是最外面开始触发的. 但是我们平常感觉好像只有 3,4,5 没有 1, 2 丫.

要触发 1, 2 就需要用到 capture 了.

上面 bubble 的例子多一层 grandparent

监听

const grandparentBox = document.querySelector(".grandparent-box")!;
const parentBox = document.querySelector(".parent-box")!;
const childBox = document.querySelector(".child-box")!;
grandparentBox.addEventListener("click", () => {
  console.log(5);
});
parentBox.addEventListener("click", () => {
  console.log(4);
});
childBox.addEventListener("click", () => {
  console.log(3);
});
parentBox.addEventListener(
  "click",
  () => {
    console.log(2);
  },
  { capture: true }
);
grandparentBox.addEventListener(
  "click",
  () => {
    console.log(1);
  },
  { capture: true }
);

我们可以把监听的顺序倒过来写。最终 console.log 顺序依然是 1 2 3 4 5. 其原理就是上面画的图, 事件触发是从最外面开始的, 只是我们一般上不适用 capture 所以会误以为只有 3,4,5 里到外.

stopPropagation

grandparentBox.addEventListener(
  "click",
  (e) => {
    console.log(1);
    e.stopPropagation();
  },
  { capture: true }
);

假如我在最外面 capture 就 stopPropagation 那么后续的 2,3,4,5 都不会触发了. 所以它的名字不叫 stopBubble. 因为 1,2 是 parent to child 并不是冒泡. 但是 stopPropagation 是一样可以阻止 "进入" 内层的.

使用场景

我好像只有在监听 document scroll 的时候会用到 capture.

removeEventListener

document.addEventListener('scroll', handleScroll, {
  passive: true,
  capture: true,
});

document.removeEventListener('scroll', handleScroll, { capture: true });

如果 add 有 capture, 那么 remove 也要声明是 capture 哦, 不然是匹配不到 remove 不掉的. 

 

Passive

参考: Improving scroll performance with passive event listeners

passive 通常用来优化体验. 比如 wheel event.

wheel event 有可能会 preventDefault 去阻止游览器滚动. 所以游览器需要等待 wheel event 执行完了才可以去滚动页面.

但是呢, 如果 wheel event 跑了很多代码, 但是没有要 preventDefault 的话, 那么这个等待就很没有意义了. 用户会感觉滚动反应慢了半拍. 

于是就有了 passive.

document.addEventListener("wheel", (e) => {
  console.log("wheel");
  e.preventDefault();
});
document.addEventListener("scroll", () => {
  console.log("scroll");
});

效果

wheel by defualt 就是 passive 的. passive 的意思就是你不能 preventDefault. 于是游览器就可以直接去滚动页面而不需要等待 wheel event 结束 (因为你不可能阻止游览器滚动了)

当加上 passive 后, scroll 就被 prevent 掉了

document.addEventListener(
  "wheel",
  (e) => {
    console.log("wheel");
    e.preventDefault(); // 有效果了
  },
  { passive: false }
);

效果

页面不会滚动, scroll event 也不会触发了.

 

Custom Event

自定义 event 是用来扩展游览器 build-in event 的. 比如游览器有 click, dblclick 但是没有 tripleclick.

那我们就可以自己扩展一个出来.

监听和触发长这样:

document.addEventListener('tripleclick', () => console.log('Hello World'));
document.dispatchEvent(new CustomEvent('tripleclick'));

要什么时候触发 tripleclick 逻辑就由我们来掌控实现了. 比如监听 click event 在一个时间内触发 3 次, 那么就 dispatch tripleclick

Custom Event 默认是不冒泡的

document.dispatchEvent(new CustomEvent('tripleclick', { bubbles: true }));

composed

composed 是用在阻止 event 跨越 Shadow DOM 的。

container 被设置了一个 Shadow DOM。

h1.addEventListener('click', () => {
  h1.dispatchEvent(new CustomEvent('test', { bubbles: true, composed: false }));
});
h1.addEventListener('test', () => {
  console.log('h1 ok'); 
});
parent.addEventListener('test', () => {
  console.log('parent ok'); 
});
container.addEventListener('test', () => {
  console.log('container ok'); 
});
document.body.addEventListener('test', () => {
  console.log('body ok');
});

h1 dispatch event,composed: false。container 和 body 收不到,因为在 Shadow DOM 外,h1 和 parent 就 ok。

passing data

event 需要能 passing data, 比如 Keyboard event 传递 key, mouse event 传递坐标.

通过 detail 属性来传值

document.addEventListener('statechange', event => {
  // get data from detail
  console.log('value', event.detail.value);
});

setTimeout(() => {
  document.dispatchEvent(
    new CustomEvent('statechange', {
      // 通过 detail passing data
      detail: {
        value: 'Hello World',
      },
      bubbles: true,
    })
  );
}, 2000);

想看 TypeScript 版本, 移步这里

 

移除 element 会自动移除事件监听?

首先,要搞清楚一件事 -- element 对象不一定要插入到 DOM Tree (document) 里面。

const button = document.createElement('button');
button.textContent = 'click me';
button.addEventListener('click', () => console.log('click'));
setInterval(() => {
  button.click();
}, 1000);

上面单纯用 JavaScript 创建了一个 button 对象,然后添加了 event listener 并且模拟 click 触发了这个 event。

完全跟 DOM Tree 无关。

接着我们把它插入 document,然后再移除

document.body.appendChild(button);
setTimeout(() => {
  button.remove();
}, 3000);

效果

从头到尾,事件监听和发布都不受插入和移除 DOM Tree 影响。

结论:事件是绑定在 element 对象上,这个对象是否插入 DOM Tree 不是重点,只有当这个 element 没用被任何地方引用,被垃圾回收的同时,它的事件才会一起被带走,其它情况我们要自己 remove event listener。

 

posted @ 2022-06-30 22:17  兴杰  阅读(128)  评论(0编辑  收藏  举报