在Shadow DOM中的setTimeout中更改了event.target?

问题描述

https://jsbin.com/qogewewomi/1/edit?html,output

<!DOCTYPE html>
<html>

<head>
  <Meta charset="utf-8">
  <Meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>
  <test-test></test-test>
  <button id='out'>Outside Shadow DOM</button>
</body>

</html>
customElements.define('test-test',class extends HTMLElement {
  constructor() {
    super();

    const node = document.createElement('template');
    node.innerHTML = '<button id="in">Inside Shadow DOM</button>';
    this.attachShadow({
      mode: 'open'
    }).appendChild(node.content);

    this.shadowRoot.querySelector('#in').addEventListener('click',e => {
      console.log(e.target);
      setTimeout(() => {
        console.log(e.target);
      });
    });
  }
});

document.querySelector('#out').addEventListener('click',e => {
  console.log(e.target);
  setTimeout(() => {
    console.log(e.target);
  });
});

我在影子DOM内外的事件侦听器中发现了这些不一致的行为。单击Inside Shadow DOM按钮后,控制台将输出

<button id="in">Inside Shadow DOM</button>
<test-test>...</test-test>

单击Outside Shadow DOM按钮时,控制台将输出

<button id="out">Outside Shadow DOM</button>
<button id="out">Outside Shadow DOM</button>

在Chrome,FireFox和Safari中进行了测试。他们都有这些不一致的行为。我不知道这是预期的行为还是错误

更新: 这个问题不应该被解决。另一个没有回答这个问题。

解决方法

这是预期的行为,不是错误。

解释将需要太多字符。

请参阅:


简而言之:

JavaScript是单线程的。
(e)Event是在所有事件处理程序周围传递的全局对象

使用SetTimeout时,Event的内容可以/将有所不同

我重写了您的测试代码:https://jsfiddle.net/CustomElementsExamples/wj1234km/

<shadow-element id="lightcoral" title=One></shadow-element>
<script>
  function log(label,color,scope,evt) {
    let composedTarget = (evt.composed && evt.composedPath());
    console.log(`%c ${label} \t%c ${evt.target.id} `,`background:${color}`,`background:${evt.target.id};color:white`,'\n\ttarget:',evt.target,"\n\tthis:",scope.nodeName || "window","\n\tcurrentTarget",evt.currentTarget,'\n\tclickedTarget:',evt.clickedTarget,"\n\tcomposed:",evt.composed ? "True" : "False","composedPath[0]:",composedTarget[0]);
  }

  customElements.define('shadow-element',class extends HTMLElement {
    constructor() {
      super().attachShadow({mode:'open'})
             .innerHTML = `<button id=green>click ${this.title}</button>`;
      let button = this.shadowRoot.querySelector('button');
      button.onclick = e => {
        let savedTarget = e.target;
        e.clickedTarget = e.target;
        button.onclick = false; //prevent double capture
        log(`clicked element:${this.id}`,'lightgreen',this,e);
        setTimeout(() => {
          log('timeout element','red;color:yellow',e)
        },500);
      };
      //this.onclick = button.onclick;
    }
  });

</script>

要输出:

target现在是<shadow-element>,因为一旦setTimeout运行 global 事件,该事件就一直沿DOM传递。

currentTarget告诉您所有事件处理均已完成
https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget

clickedTarget演示您可以在该全局事件对象(正在传递)上设置自定义属性。从而“保存”您单击的目标。但是其他事件(或下面的element.onclick函数调用)可能会覆盖它,因此最好在正确的范围内设置自定义变量savedTarget,并在您的setTimeout

中使用

您可以通过在元素本身上设置点击处理程序来查看target的变化。

target成为事件<shadow-element>冒出DOM并“逃离” shadowDOM的那一刻

,

不应关闭此问题。另一个没有回答这个问题。

是的,如果您仔细阅读的话。但由于社区不同意,我觉得我必须回答...


there的主要要点是,将对Shadow DOM中的元素触发的事件重定向了,以便从Light元素中隐藏Shadow DOM。这意味着Event对象在其生存期内将更改其target

  • 首先,在捕获阶段,尽管Event对象仍未到达Shadow DOM,但Light容器(此处为<test-test>)将成为该Event对象的目标。
  • 然后在Shadow DOM内部,它将更改为实际的Shadow目标(<button>)。
  • 最后,在从Shadow DOM退出的冒泡阶段,它将重新设置为Light容器。

因此,当您从超时记录日志时,所有事件阶段都会发生,并且事件对象将处于最终状态,并且Light容器为.target

customElements.define('test-test',class extends HTMLElement {
  constructor() {
    super();

    const node = document.createElement('template');
    node.innerHTML = '<button id="in">Inside Shadow DOM</button>';
    this.attachShadow({
      mode: 'open'
    }).appendChild(node.content);

    this.shadowRoot.addEventListener('click',e => {
      console.log("[capturing phase] in Shadow DOM",e.target);
    },{ capture: true });
    this.shadowRoot.addEventListener('click',e => {
      console.log("[bubbling phase] in Shadow DOM",{ capture: false });

  }
});

document.addEventListener('click',e => {
  console.log( "[capturing phase] in Light DOM",e.target);
},{ capture: true });
document.addEventListener('click',e => {
  console.log( "[bubbling phase] in Light DOM",{ capture: false });
<test-test></test-test>