Shadow tree 背後的想法是封裝元件的內部實作細節。
假設點擊事件發生在 <user-card> 元件的 Shadow DOM 內。但主文件中的指令碼不知道 Shadow DOM 內部,特別是如果元件來自第三方程式庫時。
因此,為了保持細節的封裝,瀏覽器會重新設定目標事件。
在 Shadow DOM 中發生的事件,在元件外部被捕捉到時,會以主機元素作為目標。
以下是簡單的範例
<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– 內部事件處理常式取得正確的目標,Shadow DOM 內的元素。 - 外部目標:
USER-CARD– 文件事件處理常式取得 Shadow 主機作為目標。
事件重新定位是一項很棒的功能,因為外部文件不必了解元件的內部結構。從其觀點來看,事件發生在 <user-card> 上。
如果事件發生在實際存在於光 DOM 中的插槽元素上,則不會發生重新定位。
例如,如果使用者在以下範例中按一下 <span slot="username">,則事件目標就是這個 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>
如果按一下 "John Smith",則對於內部和外部處理常式,目標都是 <span slot="username">。那是光 DOM 中的元素,因此不會重新定位。
另一方面,如果按一下源自陰影 DOM 的元素,例如按一下 <b>Name</b>,則當它從陰影 DOM 中冒出時,其 event.target 會重設為 <user-card>。
冒泡、event.composedPath()
對於事件冒泡的目的,會使用扁平化 DOM。
因此,如果我們有一個插槽元素,而且事件發生在其中某個地方,則它會冒泡到 <slot> 和更上層。
可以使用 event.composedPath() 取得包含所有陰影元素的原始事件目標的完整路徑。正如我們從方法名稱中所見,該路徑是在組成之後取得的。
在上述範例中,扁平化 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() 會傳回一個陣列:[span、slot、div、shadow-root、user-card、body、html、document、window]。那是組成之後,扁平化 DOM 中目標元素的父鏈。
{mode:'open'} 樹會提供陰影樹詳細資料如果陰影樹是使用 {mode: 'closed'} 建立的,則組成路徑會從主機開始:user-card 和更上層。
這是與其他使用陰影 DOM 的方法類似的原則。封閉樹的內部結構會完全隱藏起來。
event.composed
大多數事件都能成功地冒泡通過陰影 DOM 邊界。有少數事件無法做到。
這由 composed 事件物件屬性控制。如果它為 true,則事件會跨越邊界。否則,它只能從陰影 DOM 內部擷取。
如果你查看 UI 事件規格,大多數事件都有 composed: true
blur、focus、focusin、focusout、click、dblclick、mousedown、mouseupmousemove、mouseout、mouseover、滾輪,beforeinput、input、keydown、keyup。
所有觸控事件和指標事件也都有 composed: true。
不過有些事件有 composed: false
mouseenter、mouseleave(它們完全不會冒泡),load、unload、abort、error,select,slotchange.
這些事件只能在事件目標所在的同一個 DOM 中的元素上觸發。
自訂事件
當我們發送自訂事件時,我們需要將 bubbles 和 composed 屬性都設定為 true,才能讓它冒泡到組件外。
例如,這裡我們在 div#outer 的影子 DOM 中建立 div#inner,並在上面觸發兩個事件。只有 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 時,事件才會跨越影子 DOM 界線。
內建事件大多有 composed: true,如相關規格中所述
- 使用者介面事件 https://www.w3.org/TR/uievents。
- 觸控事件 https://w3c.github.io/touch-events。
- 指標事件 https://www.w3.org/TR/pointerevents。
- …等等。
一些有 composed: false 的內建事件
mouseenter、mouseleave(也不會冒泡),load、unload、abort、error,select,slotchange.
這些事件只能在同一個 DOM 中的元素上觸發。
如果我們發送一個 CustomEvent,那麼我們應該明確設定 composed: true。
請注意,在巢狀組件的情況下,一個影子 DOM 可能巢狀在另一個影子 DOM 中。在這種情況下,組成事件會冒泡到所有影子 DOM 界線。因此,如果一個事件只針對直接封裝的組件,我們也可以在影子主機上發送它,並設定 composed: false。然後它就會在組件影子 DOM 之外,但不會冒泡到更高級別的 DOM。
留言
<code>標籤,若要插入多行程式碼 - 請將它們包覆在<pre>標籤中,若要插入超過 10 行程式碼 - 請使用沙盒 (plnkr、jsbin、codepen…)