ライフサイクル

概要

LitElementベースのコンポーネントは、監視されているプロパティの変更に応じて非同期で更新されます。プロパティの変更は一括処理されます。更新が要求された後、更新が開始される前にさらにプロパティが変更されると、すべての変更が同じ更新で反映されます。

更新ライフサイクルは:

  1. プロパティがセットされる
  2. 更新が必要かどうか確認し、アップデートが必要な場合は、リクエストする
  3. 更新の実施には:
    • プロパティと属性の処理
    • 要素を描画
  4. Promiseをresolveして、更新が完了したことを示します。

LitElementとブラウザのイベントループ

これらの概念の詳細については、Jake Archibaldの記事を参照してください。

ブラウザはイベントループのタスクのキューを処理することによってJavaScriptコードを実行します。イベントループが繰り返されるたびに、ブラウザはキューからタスクを取り出し、完了まで実行します。

タスクが完了すると、キューから次のタスクを実行する前に、ブラウザはDOM更新、ユーザー対話、マイクロタスクキューなどの他のソースから作業を実行するための時間を割り当てます。

LitElementの更新は、Promiseとして非同期に要求され、マイクロタスクとしてキューに入れられます。これは、イベントループの各反復の最後に要素の更新が処理されることを意味し、更新をすばやく迅速に処理します。

Promisesと非同期関数

LitElementは、Promiseオブジェクトを使用して要素の更新をスケジュールし、それに応答します。

async awaitを使うと、Promisesで簡単に作業できます。例えば、あなたは updateComplete Promiseを待つことができます:

// `async`は関数をPromiseに戻し、` await`を使用させます
async myFunc(data) {
  // プロパティを設定し、更新をトリガする
  this.myProp = data;

  // updateCompleteのPromiseが解決するのを待つ
  await this.updateComplete;
  // ...なにか...
  return 'done';
}

async関数がPromiseを返すので、あなたもそれらを待つことができます:

let result = await myFunc('stuff');
// `result`が解決されました!次の処理に移れます。

詳細なチュートリアルについては、Web Fundamentals primer on Promisesを参照してください。

メソッドとプロパティ

更新ライフサイクルで呼び出されるメソッドの順番としては:

  1. someProperty.hasChanged
  2. requestUpdate
  3. shouldUpdate
  4. update
  5. render
  6. firstUpdated
  7. updated
  8. updateComplete

someProperty.hasChanged

宣言されたすべてのプロパティには、プロパティが設定されるたびに呼び出される関数hasChangedがあります。 hasChangedがtrueを返した場合、更新がスケジュールされます。

プロパティ変更動作をカスタマイズするためにhasChangedを設定するも参照してください。

requestUpdate

引数

 
propertyName

oldValue
更新されるプロパティの名前。

以前のプロパティ値。
返り値 Promise 更新の完了時に解決する updateComplete Promiseを返します。
更新? いいえ このメソッドの内部でのプロパティの変更は、要素の更新をトリガーしません。

hasChanged trueを返した場合、 requestUpdateが起動し、更新が行われます。

要素の更新を手動で開始するには、パラメータなしで requestUpdateを呼び出します。

プロパティオプションをサポートするカスタムプロパティセッターを実装するには、プロパティ名とその前の値をパラメーターとして渡します。

サンプル: 手動で要素の更新を開始する

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  constructor() {
    super();
    
    // イベントに応じて更新をリクエストする
    this.addEventListener('load-complete', async (e) => {
      console.log(e.detail.message);
      console.log(await this.requestUpdate());
    });
  }
  render() {
    return html`
      <button @click="${this.fire}">"load-complete"イベントが発火</button>
    `;
  }
  fire() {
    let newMessage = new CustomEvent('load-complete', { 
      detail: { message: 'こんにちは、load-completeが起きました' }
    });
    this.dispatchEvent(newMessage);
  }
}
customElements.define('my-element', MyElement);

サンプル: カスタムプロパティセッターから requestUpdateを呼び出します

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() { 
    return { prop: { type: Number } };
  }

  set prop(val) {
    let oldVal = this._prop;
    this._prop = Math.floor(val);
    this.requestUpdate('prop', oldVal);
  }

  get prop() { return this._prop; }

  constructor() {
    super();
    this._prop = 0;
  }

  render() {
    return html`
      <p>prop: ${this.prop}</p>
      <button @click="${() =>  { this.prop = Math.random()*10; }}">
        change prop
      </button>
    `;
  }
}
customElements.define('my-element', MyElement);

performUpdate

/**
 * 既定の処理を上書き
 */
performUpdate() { ... }
引数 voidPromise 更新を実施
更新? いいえ このメソッドの内部でのプロパティの変更は、要素の更新をトリガーしません。

既定では、 performUpdateは、ブラウザのイベントループにおける次の実行の終了後、マイクロタスクとしてスケジュールされます。 performUpdateをスケジュールするには、super.performUpdate()を呼び出す前にawaitする非同期メソッドとして実装します。例えば:

async performUpdate() {
  await new Promise((resolve) => requestAnimationFrame(() => resolve());
  super.performUpdate();
}

shouldUpdate

/** 
 * 既定の処理を上書き
 */
shouldUpdate(changedProperties) { ... }
引数 changedProperties Mapオブジェクトでキーは変更されたプロパティの名前です。
値は対応する以前の値です。
返り値 Boolean trueの場合、更新が進みます。デフォルトの戻り値は trueです。
更新? はい このメソッドの内部でプロパティを変更すると、要素が更新されます。

サンプル: どのプロパティの変更によって更新が発生するかをカスタマイズする

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      prop1: { type: Number },
      prop2: { type: Number }
    };
  }
  constructor() {
    super();
    this.prop1 = 0;
    this.prop2 = 0;
  }

  render() {
    return html`
      <p>prop1: ${this.prop1}</p>
      <p>prop2: ${this.prop2}</p>
      <button @click="${() => this.prop1=this.change()}">prop1を変更</button>
      <button @click="${() => this.prop2=this.change()}">prop2を変更</button>
    `;
  }

  /**
   * prop1が変更された場合にのみ要素を更新します。
   */
  shouldUpdate(changedProperties) {
    changedProperties.forEach((oldValue, propName) => { 
      console.log(`${propName} が変更されました。古い値: ${oldValue}`);
    });
    return changedProperties.has('prop1');
  }

  change() {
    return Math.floor(Math.random()*10);
  }
}
customElements.define('my-element', MyElement);

update

引数 changedProperties Mapオブジェクトでキーは変更されたプロパティの名前です。
値は対応する以前の値です。
更新? いいえ このメソッドの内部でのプロパティの変更は、要素の更新をトリガーしません。

属性値を属性に反映させて要素を更新し、 render()を呼び出します。このメソッドをオーバーライドまたは呼び出す必要はありません。

render

/** 
 * 既定の処理を上書き
 */
render() { ... }
返り値 TemplateResult lit-htmlの TemplateResultを返さなければなりません。
更新? いいえ このメソッドの内部でのプロパティの変更は、要素の更新をトリガーしません。

lit-htmlを使用して要素テンプレートをレンダリングします。

詳細については、テンプレートの作成とレンダリングのドキュメントを参照してください。

firstUpdated

/** 
 * 既定の処理を上書き
 */
firstUpdated(changedProperties) { ... }
引数 changedProperties Mapオブジェクトでキーは変更されたプロパティの名前です。
値は対応する以前の値です。
更新? はい このメソッドの内部でプロパティを変更すると、要素が更新されます。

要素のDOMが最初に更新された後、updatedが呼び出される直前に呼び出されます。

要素のテンプレートが作成された後にワンタイム作業を実行するように firstUpdatedをカスタマイズします。

サンプル: 最初の更新時に入力要素をフォーカスする

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      textAreaId: { type: String },
      startingText: { type: String }
    };
  }
  constructor() {
    super();
    this.textAreaId = 'サンプルテキスト';
    this.startingText = '最初の読み込みでフォーカスされます';
  }
  render() {
    return html`
      <textarea id="${this.textAreaId}">${this.startingText}</textarea>
    `;
  }
  firstUpdated(changedProperties) {
    changedProperties.forEach((oldValue, propName) => { 
      console.log(`${propName}が更新されました。 古い値: ${oldValue}`);
    });
    const textArea = this.shadowRoot.getElementById(this.textAreaId);
    textArea.focus();
  }
}
customElements.define('my-element', MyElement);

updated

/** 
 * 既定の処理を上書き
 */
updated(changedProperties) { ... }
引数 changedProperties Mapオブジェクトでキーは変更されたプロパティの名前です。
値は対応する以前の値です。
更新? はい このメソッドの内部でプロパティを変更すると、要素が更新されます。

要素のDOMが更新されレンダリングされたときに呼び出されます。更新後の処理を実装してください。

サンプル: 更新後に要素をフォーカスする

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      prop1: { type: Number },
      prop2: { type: Number }
    };
  }
  constructor() {
    super();
    this.prop1 = 0;
    this.prop2 = 0;
  }
  render() {
    return html`
      <style>button:focus { background-color: aliceblue; }</style>

      <p>prop1: ${this.prop1}</p>
      <p>prop2: ${this.prop2}</p>

      <button id="a" @click="${() => this.prop1=Math.random()}">prop1</button>
      <button id="b" @click="${() => this.prop2=Math.random()}">prop2</button>
    `;
  }
  updated(changedProperties) {
    changedProperties.forEach((oldValue, propName) => { 
      console.log(`${propName}が更新されました。元の値は: ${oldValue}`);
    });
    let b = this.shadowRoot.getElementById('b');
    b.focus();
  }
}
customElements.define('my-element', MyElement);

updateComplete

  Promise 要素の更新が終了すると、 Booleanが返されます。
Resolves

より多くの保留中の更新がなければ trueを返します。

この更新サイクルが別の更新を引き起こした場合は falseを返します。
 

updateComplete Promiseは、要素の更新が完了した時点をPromiseがresolveされます。更新を待つにはupdateCompleteを使ってください:

  await updateComplete;
  // なにか
  updateComplete.then(() => { /* なにか */ });

updateCompleteをresolveする前に追加状態を待たせるには、 updateCompleteゲッターを実装してください:

  get updateComplete() {
    return this.getMoreState().then(() => {
      return this._updatePromise;
    });
  }

サンプル

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      prop1: { type: Number }
    };
  }

  constructor() {
    super();
    this.prop1 = 0;
  }

  render() {
    return html`
      <p>prop1: ${this.prop1}</p>
      <button @click="${this.changeProp}">prop1</button>
    `;
  }

  async getMoreState() {
    return;
  }

  async changeProp() {
    this.prop1 = Math.random();
    await Promise.all(this.updateComplete, this.getMoreState());
    console.log('更新完了。その他の状態も完了しました。');
  }
}

customElements.define('my-element', MyElement);

サンプルコード

更新をトリガするプロパティの変更動作をカスタマイズする

shouldUpdateを実装する:

shouldUpdate(changedProps) {
  return changedProps.has('prop1');
}

更新の元となるそれぞれのプロパティ変更をカスタマイズする

プロパティにhasChangedを指定する:

static get properties(){ return {
  myProp: {
    type: Number,
    /* newVal> oldValの場合のみmyPropを変更したとみなします */
    hasChanged(newVal, oldVal) {
      return newVal > oldVal;
    }
  }
}

Customize what constitutes a property change

Specify hasChanged for the property. See the Properties documentation.

オブジェクトのサブプロパティのプロパティの変更と更新の管理

Mutation(オブジェクトのサブプロパティや配列アイテムの変更)は検知できません。代わりに、オブジェクト全体を書き換えるか、または変更後にrequestUpdateを呼び出します。

// Option 1: オブジェクト全体を書き直し、更新をトリガする
this.prop1 = Object.assign({}, this.prop1, { subProp: 'data' });

// Option 2: サブプロパティを変更してから、requestUpdateを呼び出します。
this.prop1.subProp = 'data';
this.requestUpdate();

プロパティ変更でない時の更新

requestUpdateを呼ぶ:

// イベントに応じて更新をリクエストする
this.addEventListener('load-complete', async (e) => {
  console.log(e.detail.message);
  console.log(await this.requestUpdate());
});

プロパティの変更に関係なく更新を要求する

requestUpdate()を呼ぶ:

this.requestUpdate();

特定のプロパティの更新をリクエストする

requestUpdate(propName, oldValue)を呼ぶ:

let oldValue = this.prop1;
this.prop1 = '新しい値';
this.requestUpdate('prop1', oldValue);

最初の更新後に何かをする

firstUpdatedを実装する:

firstUpdated(changedProps) {
  console.log(changedProps.get('prop1'));
}

更新のたびに何かをする

updatedを実装する:

updated(changedProps) {
  console.log(changedProps.get('prop1'));
}

要素が次回更新されたときに何かする

updateCompleteのPromiseをawaitする:

await updateComplete;
// なにか
updateComplete.then(() => {
  // なにか
});

要素の更新が完了するまで待つ

updateCompleteのPromiseをawaitする:

let done = await updateComplete;
updateComplete.then(() => {
  // アップデートの完了
});