Javascript Shadow DOM 和事件(events)

2023-02-17 10:59 更新

Shadow tree 背后的思想是封裝組件的內部實現(xiàn)細節(jié)。

假設,在 <user-card> 組件的 shadow DOM 內觸發(fā)一個點擊事件。但是主文檔內部的腳本并不了解 shadow DOM 內部,尤其是當組件來自于第三方庫。

所以,為了保持細節(jié)簡單,瀏覽器會重新定位(retarget)事件。

當事件在組件外部捕獲時,shadow DOM 中發(fā)生的事件將會以 host 元素作為目標。

這里有個簡單的例子:

<user-card></user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<p>
      <button>Click me</button>
    </p>`;
    this.shadowRoot.firstElementChild.onclick =
      e => alert("Inner target: " + e.target.tagName);
  }
});

document.onclick =
  e => alert("Outer target: " + e.target.tagName);
</script>

如果你點擊了 button,就會出現(xiàn)以下信息:

  1. Inner target: ?BUTTON? —— 內部事件處理程序獲取了正確的目標,即 shadow DOM 中的元素。
  2. Outer target: ?USER-CARD? —— 文檔事件處理程序以 shadow host 作為目標。

事件重定向是一件很棒的事情,因為外部文檔并不需要知道組件的內部情況。從它的角度來看,事件是發(fā)生在 <user-card>

如果事件發(fā)生在 slotted 元素上,實際存在于 light DOM 上,則不會發(fā)生重定向。

例如,在下面的例子中,如果用戶點擊了 <span slot="username">,那么對于 shadow 和 light 處理程序來說,事件目標就是當前這個 span 元素。

<user-card id="userCard">
  <span slot="username">John Smith</span>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<div>
      <b>Name:</b> <slot name="username"></slot>
    </div>`;

    this.shadowRoot.firstElementChild.onclick =
      e => alert("Inner target: " + e.target.tagName);
  }
});

userCard.onclick = e => alert(`Outer target: ${e.target.tagName}`);
</script>

如果單擊事件發(fā)生在 "John Smith" 上,則對于內部和外部處理程序來說,其目標是 <span slot="username">。這是 light DOM 中的元素,所以沒有重定向。

另一方面,如果單擊事件發(fā)生在源自 shadow DOM 的元素上,例如,在 <b>Name</b> 上,然后當它冒泡出 shadow DOM 后,其 event.target 將重置為 <user-card>。

冒泡(bubbling), event.composedPath()

出于事件冒泡的目的,使用扁平 DOM(flattened DOM)。

所以,如果我們有一個 slot 元素,并且事件發(fā)生在它的內部某個地方,那么它就會冒泡到 <slot> 并繼續(xù)向上。

使用 event.composedPath() 獲得原始事件目標的完整路徑以及所有 shadow 元素。正如我們從方法名稱中看到的那樣,該路徑是在組合(composition)之后獲取的。

在上面的例子中,扁平 DOM 是:

<user-card id="userCard">
  #shadow-root
    <div>
      <b>Name:</b>
      <slot name="username">
        <span slot="username">John Smith</span>
      </slot>
    </div>
</user-card>

因此,對于 <span slot="username"> 上的點擊事件,會調用 event.composedPath() 并返回一個數(shù)組:[spanslotdivshadow-rootuser-cardbodyhtmldocumentwindow]。在組合之后,這正是扁平 DOM 中目標元素的父鏈。

Shadow 樹詳細信息僅提供給 ?{mode:'open'}? 樹

如果 shadow 樹是用 {mode: 'closed'} 創(chuàng)建的,那么組合路徑就從 host 開始:user-card 及其更上層。

這與使用 shadow DOM 的其他方法的原理類似。closed 樹內部是完全隱藏的。

event.composed

大多數(shù)事件能成功冒泡到 shadow DOM 邊界。很少有事件不能冒泡到 shadow DOM 邊界。

這由 composed 事件對象屬性控制。如果 composed 是 true,那么事件就能穿過邊界。否則它僅能在 shadow DOM 內部捕獲。

如果你瀏覽一下 UI 事件規(guī)范 就知道,大部分事件都是 composed: true

  • blur,focusfocusin,focusout,
  • click,dblclick
  • mousedown,mouseup mousemovemouseout,mouseover
  • wheel,
  • beforeinput,input,keydownkeyup。

所有觸摸事件(touch events)及指針事件(pointer events)都是 composed: true。

但也有些事件是 composed: false 的:

  • mouseenter,mouseleave(它們根本不會冒泡),
  • loadunload,abort,error
  • select,
  • slotchange。

這些事件僅能在事件目標所在的同一 DOM 中的元素上捕獲,

自定義事件(Custom events)

當我們發(fā)送(dispatch)自定義事件,我們需要設置 bubbles 和 composed 屬性都為 true 以使其冒泡并從組件中冒泡出來。

例如,我們在 div#outer shadow DOM 內部創(chuàng)建 div#inner 并在其上觸發(fā)兩個事件。只有 composed: true 的那個自定義事件才會讓該事件本身冒泡到文檔外面:

<div id="outer"></div>

<script>
outer.attachShadow({mode: 'open'});

let inner = document.createElement('div');
outer.shadowRoot.append(inner);

/*
div(id=outer)
  #shadow-dom
    div(id=inner)
*/

document.addEventListener('test', event => alert(event.detail));

inner.dispatchEvent(new CustomEvent('test', {
  bubbles: true,
  composed: true,
  detail: "composed"
}));

inner.dispatchEvent(new CustomEvent('test', {
  bubbles: true,
  composed: false,
  detail: "not composed"
}));
</script>

總結

事件僅僅是在它們的 composed 標志設置為 true 的時候才能通過 shadow DOM 邊界。

內建事件大部分都是 composed: true 的,正如相關規(guī)范所描述的那樣:

也有些內建事件它們是 composed: false 的:

  • mouseenter,mouseleave(也不冒泡),
  • load,unloadabort,error,
  • select
  • slotchange。

這些事件僅能在同一 DOM 中的元素上捕獲。

如果我們發(fā)送一個 CustomEvent,那么我們應該顯式地設置 composed: true。

請注意,如果是嵌套組件,一個 shadow DOM 可能嵌套到另外一個 shadow DOM 中。在這種情況下合成事件冒泡到所有 shadow DOM 邊界。因此,如果一個事件僅用于直接封閉組件,我們也可以在 shadow host 上發(fā)送它并設置 composed: false。這樣它就不在組件 shadow DOM 中,也不會冒泡到更高級別的 DOM。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號