同期的に子要素のアップグレードを待って処理する
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ 親要素と子要素が同時に DOM にアタッチされると親からアップグレードされる
◆ 子要素を待とうと非同期にするとアタッチした処理の直後に初期化は完了してない
◆ innerHTML や append だと Promise を返すことも出来ない
◆ 子要素がアップグレード時にイベント発生させて全てアップグレードされたら初期化を行う
◆ 子要素を待とうと非同期にするとアタッチした処理の直後に初期化は完了してない
◆ innerHTML や append だと Promise を返すことも出来ない
◆ 子要素がアップグレード時にイベント発生させて全てアップグレードされたら初期化を行う
問題点
以前カスタム要素のアップグレードがやっかいという記事を書きました簡単にまとめると 親要素の初期化処理をするときにまだ子要素がアップグレードされておらずメソッドやプロパティがない状態のことがあります
ちゃんと依存する子要素の定義ファイルをインポートしていれば定義済みではあるので非同期にするだけでおっけいです
動的なものでインポートされてない場合があるなら whenDefined を使って待つ必要があります
初期化処理が非同期になると色々不便なところがあって 引きづられて非同期が必要ないはずなのに非同期にしないと順番がおかしくなるケースがあったり 初期化完了したかが分からないデメリットがあります
この問題が起きるのは親子でカスタム要素のものが同時に作られた場合です
document.body.innerHTML = `<x-parent><x-child></x-child></x-parent>`
のように一度に作ると x-parent から初期化されて
parent constructor
parent connected
child constructor
child connected
という順番で処理されます
JavaScript でそれぞれの要素を作る場合は
const p = document.createElement("x-parent")
const c = document.createElement("x-child")
p.append(c)
document.body.append(p)
parent constructor
child constructor
parent connected
child connected
順にコンストラクタが実行されて DOM にアタッチされたときに connectedCallback は親から順に呼び出されます
ですがコンストラクタが呼び出されるタイミングでアップグレードは完了しています
親要素の connectedCallback の時点で子要素のメソッドは使えます
また 親要素が初期化処理で子要素を作る場合は innerHTML を書き換えたり append したときに同期的にコンストラクタと connectedCallback が呼び出されます
append などの直後に子要素にアクセスすればそのときにはメソッドは使用可能です
なので DOM にまとめて要素が親子揃って作られた時のみの問題です
同期的に処理する
子要素のアップグレードを待つために非同期にすると 親子の要素を DOM にアタッチした側でアタッチ直後にまだ初期化が終わってませんアップグレードは終わっていますが通常は connectedCallback もどちらも終わっているはずなのに子要素を待つせいでこれらを使う側まで不便になります
document.body.innerHTML = `<x-parent><x-child></x-child></x-parent>`
console.log("非同期にしてるとこのタイミングでは connectedCallback の処理が完了してない")
これが嫌なので同期的に扱えるようにします
class BaseElement extends HTMLElement {
constructor() {
super()
this._upgraded = true
this.dispatchEvent(new CustomEvent("element-upgraded", { detail: this }))
}
}
customElements.define(
"x-parent",
class extends BaseElement {
connectedCallback() {
console.log("parent-connected")
let count = 0
const listener = eve => {
eve.currentTarget.removeEventListener("element-upgraded", listener)
--count || this.onReadyChildren()
}
for (const elem of this.children) {
if (!elem._upgraded && elem.localName.includes("-")) {
count++
elem.addEventListener("element-upgraded", listener)
}
}
}
onReadyChildren() {
console.log("parent-children-ready")
this.firstElementChild.method()
}
}
)
customElements.define(
"x-child",
class extends BaseElement {
connectedCallback() {
console.log("child-connected")
}
method() {
console.log("child-method")
}
}
)
document.body.innerHTML = `
<x-parent>
<x-child>a</x-child>
<x-child>b</x-child>
</x-parent>
`
console.log("innerHTML set")
parent-connected
child-connected
parent-children-ready
child-method
child-connected
innerHTML set
「innerHTML set」 が表示される前に 「parent-children-ready」 や 「child-method」 が表示されています
やっていることはイベントを listen して最後の要素の処理が終わったら関数を呼び出すという昔からある方法です
共通のベースクラスを用意してそこでコンストラクタが実行されたら element-upgraded イベントを起こすようにしています
またアップグレード済みを表すフラグもセットします
親要素では初期処理をする時に未アップグレードの要素にリスナを付けてその数をカウントします
アップグレードされるたびにリスナが呼び出されてカウントが減り最後の要素がアップグレードされると 親要素の初期化処理が行われます
イベントリスナは同期的なので 「innerHTML set」 が出るより先に行われます
タイミングはコンストラクタなので 2 つ目の connectedCallback より先に親の初期化処理が実行されます
子要素の connectedCallback を待ちたいなら connectedCallback で initialized イベントを起こすようにしてそっちを listen すれば connectedCallback の後に親要素の処理を実行できます