Javascript Shadow DOM 和事件(events)

2023-02-17 10:59 更新

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

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

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

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

這里有個簡單的例子:

<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? —— 內(nèi)部事件處理程序獲取了正確的目標(biāo),即 shadow DOM 中的元素。
  2. Outer target: ?USER-CARD? —— 文檔事件處理程序以 shadow host 作為目標(biāo)。

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

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

例如,在下面的例子中,如果用戶點擊了 <span slot="username">,那么對于 shadow 和 light 處理程序來說,事件目標(biāo)就是當(dāng)前這個 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" 上,則對于內(nèi)部和外部處理程序來說,其目標(biāo)是 <span slot="username">。這是 light DOM 中的元素,所以沒有重定向。

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

冒泡(bubbling), event.composedPath()

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

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

使用 event.composedPath() 獲得原始事件目標(biāo)的完整路徑以及所有 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"> 上的點擊事件,會調(diào)用 event.composedPath() 并返回一個數(shù)組:[spanslotdivshadow-rootuser-cardbodyhtmldocumentwindow]。在組合之后,這正是扁平 DOM 中目標(biāo)元素的父鏈。

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

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

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

event.composed

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

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

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

  • blur,focus,focusin,focusout,
  • click,dblclick,
  • mousedown,mouseup mousemovemouseout,mouseover
  • wheel,
  • beforeinput,input,keydown,keyup。

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

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

  • mouseentermouseleave(它們根本不會冒泡),
  • loadunload,aborterror,
  • select,
  • slotchange。

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

自定義事件(Custom events)

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

例如,我們在 div#outer shadow DOM 內(nèi)部創(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>

總結(jié)

事件僅僅是在它們的 composed 標(biāo)志設(shè)置為 true 的時候才能通過 shadow DOM 邊界。

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

也有些內(nèi)建事件它們是 composed: false 的:

  • mouseenter,mouseleave(也不冒泡),
  • loadunload,abort,error
  • select,
  • slotchange

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

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

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


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號