事件隔离

 Shadow DOM,它不仅仅可以做到 DOM 元素的 CSS 样式隔离,还可以做到事件的隔离处理,例如:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <template id="custom-element-template">
      <h1>Shadow DOM 中的标题</h1>
      <!-- button 按钮会渲染在 Shadow DOM 中 -->
      <button onclick="handleShadowButtonClick(event)">
        Shadow DOM 中的按钮
      </button>
    </template>
 
    <!-- 普通 DOM 测试-->
    <div id="outer" onclick="handleOuterClick(event)">
      <h1>普通 DOM 中的标题</h1>
      <!-- button 按钮在普通 DOM 下  --->
      <button onclick="handleButtonClick(event)">普通 DOM 中的按钮</button>
    </div>
 
    <!-- Shadow DOM 测试-->
    <div id="shadow-outer" onclick="handleShadowOuterClick(event)">
      <!-- Shadow Host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上 -->
      <!-- template 中的 button 按钮元素的 DOM 副本会被拷贝到该 Shadow Host 下 --->
      <custom-element onclick="handleShadowHostClick(event)"></custom-element>
    </div>
 
    <script>
      // 普通 DOM 事件
      function handleButtonClick(e) {
        console.log("handleButtonClick: ", e.target);
      }
 
      // 普通 DOM 事件
      function handleOuterClick(e) {
        console.log("handleOuterClick: ", e.target);
      }
 
      // Shadow DOM 事件
      function handleShadowButtonClick(e) {
        console.log("handleShadowButtonClick: ", e.target);
      }
 
      // Shadow DOM 事件
      function handleShadowHostClick(e) {
        console.log("handleShadowHostClick: ", e.target);
      }
 
      // 普通 DOM 事件
      function handleShadowOuterClick(e) {
        console.log("handleShadowOuterClick: ", e.target);
      }
 
      // 全局 document 委托事件
      document.onclick = function (e) {
        console.log("document.onclick: ", e.target);
      };
 
      class CustomElement extends HTMLElement {
        constructor() {
          super();
          // Shadow Root: Shadow Tree 的根节点
          const shadowRoot = this.attachShadow({ mode: "open" });
          const $template = document.getElementById("custom-element-template");
          // cloneNode:
          // 克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件 (比如 onclick="alert(1)"),
          // 但不会拷贝那些使用 addEventListener() 方法或者 node.onclick = fn 这种用 JavaScript 动态绑定的事件。
          shadowRoot.appendChild($template.content.cloneNode(true));
        }
      }
      customElements.define("custom-element", CustomElement);
    </script>
  </body>
</html>.

这就会导致一个问题:尽管 DOM 事件不会被阻断,但是 Shadow Root 之上的父节点已经无法获取精准的事件对象。

熟悉 React 开发的同学,会发现 React 17 以下会使用 Document 进行事件委托处理,此时会因为拿不到 Shadow DOM 中的事件对象,而导致事件失效。为了解决类似的问题,React 17 不再使用 Document 进行事件委托,而是使用 React 挂载的 Root 节点进行事件委托,此时如果在 Shadow DOM 中使用 React 框架,那么事件可以被正确处理