自訂指令

指令是函數,可以透過自訂模板表達式的渲染方式來擴展 Lit。指令非常有用且強大,因為它們可以是有狀態的、可以存取 DOM、可以在模板斷開連線和重新連線時收到通知,並且可以在渲染呼叫之外獨立更新表達式。

在您的模板中使用指令就像在模板表達式中呼叫函數一樣簡單

Lit 隨附許多內建指令,例如 repeat()cache()。使用者也可以編寫自己的自訂指令。

指令有兩種

  • 簡單函數
  • 基於類別的指令

簡單函數會傳回要渲染的值。它可以接受任意數量的引數,或者不接受任何引數。

基於類別的指令可讓您執行簡單函數無法執行的操作。使用基於類別的指令來

  • 直接存取渲染的 DOM (例如,新增、移除或重新排序渲染的 DOM 節點)。
  • 在渲染之間保留狀態。
  • 在渲染呼叫之外非同步更新 DOM。
  • 在指令與 DOM 斷開連線時清除資源

本頁的其餘部分說明基於類別的指令。

若要建立基於類別的指令

  • 將指令實作為一個類別,該類別會擴展 Directive 類別。
  • 將您的類別傳遞給 directive() 工廠,以建立可用於 Lit 模板表達式的指令函數。

評估此模板時,指令函數 (hello()) 會傳回 DirectiveResult 物件,該物件會指示 Lit 建立或更新指令類別 (HelloDirective) 的執行個體。然後,Lit 會呼叫指令執行個體的方法來執行其更新邏輯。

有些指令需要非同步更新 DOM,在正常更新週期之外。若要建立非同步指令,請擴展 AsyncDirective 基底類別,而不是 Directive。如需詳細資訊,請參閱 非同步指令

指令類別具有一些內建的生命週期方法

  • 類別建構函式,用於一次性初始化。
  • render(),用於宣告式渲染。
  • update(),用於命令式 DOM 存取。

您必須為所有指令實作 render() 回呼。實作 update() 是選用的。update() 的預設實作會呼叫 render() 並傳回其值。

非同步指令可以在正常更新週期之外更新 DOM,使用一些額外的生命週期回呼。如需詳細資訊,請參閱 非同步指令

當 Lit 第一次在表達式中遇到 DirectiveResult 時,它會建構對應指令類別的執行個體 (導致執行指令的建構函式和任何類別欄位初始設定式)

只要在每次渲染時在相同的表達式中使用相同的指令函數,就會重複使用先前的執行個體,因此執行個體的狀態會在渲染之間持續存在。

建構函式會接收單一 PartInfo 物件,其中提供有關使用指令的表達式的中繼資料。這對於在指令僅設計用於特定類型的表達式時提供錯誤檢查非常有用 (請參閱 將指令限制為一種表達式類型)。

render() 方法應傳回要渲染到 DOM 中的值。它可以傳回任何可渲染的值,包括另一個 DirectiveResult

除了參照指令執行個體上的狀態之外,render() 方法還可以接受傳遞到指令函數中的任意引數

render() 方法定義的參數會決定指令函數的簽章

在更進階的使用案例中,您的指令可能需要存取基礎 DOM 並以命令方式讀取或變更它。您可以透過覆寫 update() 回呼來達成此目的。

update() 回呼會接收兩個引數

  • 具有直接管理與表達式相關聯之 DOM 的 API 的 Part 物件。
  • 包含 render() 引數的陣列。

您的 update() 方法應傳回 Lit 可以渲染的內容,如果不需要重新渲染,則傳回特殊值 noChangeupdate() 回呼相當有彈性,但典型用法包括

  • 從 DOM 讀取資料,並使用它來產生要渲染的值。
  • 使用 Part 物件上的 elementparentNode 參照來以命令方式更新 DOM。在此情況下,update() 通常會傳回 noChange,表示 Lit 不需要採取任何進一步的動作來渲染指令。

每個表達式位置都有其自己的特定 Part 物件

  • ChildPart 用於 HTML 子位置中的表達式。
  • AttributePart 用於 HTML 屬性值位置中的表達式。
  • BooleanAttributePart 用於布林值屬性值 (名稱前置詞為 ?) 中的表達式。
  • EventPart 用於事件接聽器位置 (名稱前置詞為 @) 中的表達式。
  • PropertyPart 用於屬性值位置 (名稱前置詞為 .) 中的表達式。
  • ElementPart 用於元素標籤上的表達式。

除了 PartInfo 中包含的特定部分中繼資料之外,所有 Part 類型都提供對與表達式相關聯之 DOM element (或 ChildPartparentNode) 的存取,該 DOM 可以在 update() 中直接存取。例如

此外,directive-helpers.js 模組包含許多對 Part 物件起作用的協助程式函數,可用於在指令的 ChildPart 中動態建立、插入和移動部分。

update() 的預設實作只會呼叫並傳回 render() 的值。如果您覆寫 update() 並且仍然想要呼叫 render() 來產生值,您需要明確呼叫 render()

render() 引數會以陣列的形式傳遞到 update() 中。您可以這樣將引數傳遞給 render()

雖然 update() 回呼比 render() 回呼更強大,但有一個重要的區別:當使用 @lit-labs/ssr 套件進行伺服器端渲染 (SSR) 時,只有在伺服器上呼叫 render() 方法。為了與 SSR 相容,指令應該從 render() 傳回值,並且僅將 update() 用於需要存取 DOM 的邏輯。

有時候,指令可能沒有新的內容供 Lit 渲染。您可以透過從 update()render() 方法傳回 noChange 來表示此情況。這與傳回 undefined 不同,後者會導致 Lit 清除與指令相關聯的 Part。傳回 noChange 會讓先前渲染的值保留在原處。

傳回 noChange 有幾個常見原因

  • 根據輸入值,沒有新內容要渲染。
  • update() 方法以命令方式更新了 DOM。
  • 在非同步指令中,對 update()render() 的呼叫可能會傳回 noChange,因為尚未有內容要渲染。

例如,指令可以追蹤傳遞給它的先前值,並執行自己的髒檢查,以判斷是否需要更新指令的輸出。update()render() 方法可以傳回 noChange,表示不需要重新渲染指令的輸出。

有些指令僅在一個內容中才有用,例如屬性表達式或子表達式。如果放置在錯誤的內容中,指令應擲回適當的錯誤。

例如,classMap 指令會驗證它是否僅在 AttributePart 中使用,並且僅適用於 class 屬性

先前的範例指示是同步的:它們從其 render()/update() 生命周期回呼中同步傳回值,因此它們的結果會在元件的 update() 回呼期間寫入 DOM。

有時,您會希望指示能夠非同步更新 DOM,例如,當它依賴於非同步事件(如網路請求)時。

若要非同步更新指示的結果,指示需要擴展 AsyncDirective 基礎類別,該類別提供 setValue() API。setValue() 允許指示在範本的正常 update/render 週期之外,將新值「推送」到其範本表達式中。

以下是一個簡單的非同步指示範例,它會渲染 Promise 值

在這裡,渲染的範本顯示「等待 Promise 解析」,然後顯示 Promise 的解析值,無論何時解析。

非同步指示通常需要訂閱外部資源。為防止記憶體洩漏,非同步指示應在不再使用指示實例時取消訂閱或處置資源。為此,AsyncDirective 提供了以下額外的生命週期回呼和 API

  • disconnected():當不再使用指示時呼叫。指示實例在以下三種情況下會斷開連線

    • 當指示所在的 DOM 樹從 DOM 中移除時
    • 當指示的主機元素斷開連線時
    • 當產生指示的表達式不再解析為相同的指示時。

    在指示收到 disconnected 回呼後,應釋放它在 updaterender 期間可能訂閱的所有資源,以防止記憶體洩漏。

  • reconnected():當先前斷開連線的指示恢復使用時呼叫。由於 DOM 子樹可以暫時斷開連線,然後稍後重新連線,因此斷開連線的指示可能需要對重新連線做出反應。這方面的範例包括當 DOM 被移除並快取以供稍後使用,或者當主機元素移動導致斷開連線和重新連線時。 reconnected() 回呼應始終與 disconnected() 一起實作,以便將斷開連線的指示恢復到其工作狀態。

  • isConnected:反映指示的目前連線狀態。

請注意,如果重新渲染其包含的樹狀結構,則 AsyncDirective 可能會在斷開連線時繼續接收更新。因此,update 和/或 render 應始終檢查 this.isConnected 標誌,然後再訂閱任何長期保留的資源,以防止記憶體洩漏。

以下是一個訂閱 Observable 並適當處理斷開連線和重新連線的指示範例