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
、mouseup
mousemove
、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…)