本系列一共七章,Github 地址請查閱這裏,原文地址請查閱這裏。javascript
這是編寫一個 JavaScript 框架系列的第六章。本章,我將會討論自定義元素的好處和它們在現代前端框架核心內的可能角色。html
近些年組件風靡整個網絡。全部的現代前端框架諸如 React,Vue 或者 Polymer - 都使用基於模塊化的組件。它們提供了不一樣的 API 而且底層工做方式不一致,然而他們和其它的最新的框架有一些相同的如下功能。前端
直到最近,這些功能還缺乏了一個簡單的原生 API ,可是這隨着 Custom Elements spec 的定稿而改變。自定義元素能夠涵蓋以上功能,但它們並不老是完美的匹配。讓我們走着瞧^.^。html5
自定義元素是 Web Components standard 的一部分,它在 2011 被提議,且在最近穩定前出臺了的兩個不一樣規範。最終定稿感受是一個簡單原生的組件化框架替代品而不是框架做者的工具。它爲定義組件提供了一個漂亮的高階 API, 但它缺乏不需用墊片的功能(須要兼容插件來支持)。java
若是您還不熟悉自定義元素,請在繼續以前查看本文。git
自定義元素 API 是基於 ES6 類的。元素能夠由原生的 HTML 元素或者自定義元素繼承而來,而且它們能夠用新的屬性和方法擴展。他們也能夠重寫一系列的方法-定義在規範中-能夠做爲他們生命週期的鉤子。github
class MyEelement extends HTMLElement {
// these are standard hooks, called on certain events
constructor() { ... }
connectedCallback () { ... }
disconnectedCallback () { ... }
adoptedCallback () { ... }
attributeChangedCallback (attrName, oldVal, newVal) { ... }
// these are custom methods and properties
get myProp () { ... }
set myProp () { ... }
myMethod () { ... }
}
// this registers the Custom Element
customElements.define('my-element', MyElement)
複製代碼
在定義以後,這些元素能夠在 HTML 或者 JavaScript 代碼中以名稱實例化。web
<my-element></my-element>
api
基於類的 API 很是簡潔,可是在我看來,它缺乏靈活性。做爲框架做者,我更加喜歡棄用的 v0 API,它是基於老舊的經典原型方法的。瀏覽器
const MyElementProto = Object.create(HTMLElement.prototype)
// native hooks
MyElementProto.attachedCallback = ...
MyElementProto.detachedCallback = ...
// custom properties and methods
MyElementProto.myMethod = ...
document.registerElement('my-element', { prototype: MyElementProto })
複製代碼
它大概不夠優雅,可是它能夠把 ES6 和 ES6 規範以前的代碼很好地整合在一塊兒。從另外一方面說,把 ES6 規範以前的代碼和類代碼混合在一塊兒使用會是至關複雜的。
好比,我想要有能力控制組件繼承哪一個 HTML 接口。ES6 類使用靜態的 extends
關鍵字來繼承,而且它們要求開發者輸入 MyClass extends ChosenHTMLInterface
。
這很是不適用於我目前的使用狀況,由於 NX 基於中間件函數而不是類。在 NX 中,能夠用 element
配置屬性來設置接口,接口接受一個有效的 HTML元素名稱好比 - button
。
nx.component({element: 'button'})
.register('my-button')
複製代碼
爲了達到這一目標,我不得不使用基於原型的系統來模仿 ES6 類。長話短說,操做起來比人所能想的要讓人蛋疼,而且它須要不需墊片的 ES6 Reflect.construct
和性能殺手 Object.setPrototypeOf
函數。
function MyElement() {
return Reflect.construct(HTMLELEMENT, [], MyElement)
}
const myProto = MyElement.prototype
Object.setPrototypeOf(myProto, HTMLElement.prototype)
Object.setPrototypeOf(MyElement, HTMLElement)
myProto.connectedCallback = ...
myProto.disconnectedCallback = ...
customElements.define('my-element', MyElement)
複製代碼
這只是我發如今使用 ES6 類的很困難的狀況之一。對於平常應用,我以爲他們是很是好的,可是當我想要很裝逼地充分利用這門語言的功能的時候,我更傾向於使用原型繼承。
自定義元素擁有五個生命週期鉤子,會在特定事件觸發的時候同步調用。
constructor
會在元素的實例化的過程被調用connectedCallback
會在元素被掛載到 DOM 的時候調用disconnectedCallback
會在元素被從 DOM 中移除的時候調用adoptedCallback
會在當使用 importNode
或者 cloneNode
把元素掛載到一個新的文檔之中的時候調用attributeChangedCallback
會在當被監聽的元素屬性發生變化的時候被調用constructor
和 connectedCallback
很是適合建立組件狀態和邏輯,而 attributeChangedCallback
能夠被用來以 HTML 屬性來顯示組件的屬性,反之亦然。disconnectedCallback
用來在組件銷燬後清理內存。
整合在一塊兒,這些涵蓋了一系列很好的功能,可是我仍然忽略了 beforeDisconnected
和 childrenChanged
回調。beforeDisconnected
鉤子適用於簡單的離開動畫,然而除了封裝或者大幅修改 DOM 是沒法實現它的。
childrenChanged
鉤子對於橋接狀態和視圖是很是重要的。看下如下示例:
nx.component()
.use((elem, state) => state.name = 'World')
.register('my-element')
複製代碼
<my-component>
<p>Hello: ${name}<p>
</my-component>
複製代碼
這是一個簡單的模板片斷,把 name
屬性值從狀態插入到視圖中。當用戶決定置換 p
元素爲其它元素時,框架會接收到改變的通知。它不得不清理老的 p
元素內容,而後把插值插入到新內容中。childrenChanged
可能不會公開爲開發者鉤子,可是知道什麼時候組件內容發生改變是一個框架必備的功能。
如我所述,自定義元素缺乏一個 childrenChanged
回調,可是可使用老舊的 MutationObserver API 來實現。MutationObservers 也爲老瀏覽器提供了 connectedCallback
,disconnectedCallback
和 attributeChangedCallback
鉤子的替代品。
// create an observer instance
const observer = new MutationObserver(onMutations)
function onMutations (mutations) {
for (let mutation of mutations) {
// handle mutation.addedNodes, mutation.removedNodes, mutation.attributeName and mutation.oldValue here
}
}
// listen for attribute and child mutations on `MyComponentInstance` and all of its ancestors
observer.observe(MyComponentInstance, {
attributes: true,
childList: true,
subtree: true
})
複製代碼
除了自定義元素的簡潔 API,這將會產生一些自定義元素必要性的問題。
下一章節,我將會闡述 MutationObservers 和 自定義元素的一些關鍵區別以及使用的場景。
當發生 DOM 改變的時候,自定義元素回調會同步調用,而 MutationObservers 收集這些改變並異步調用其中的一批。對於組織邏輯這並非什麼大問題,可是它會在內存清理階段引起一些不可預見的 bugs。當待處理的數據還存在時,有一個小的時間間隔是危險的。
另外一個重要的區別是, MutationObservers 沒有進入 shadow DOM 邊界。監聽 shadow DOM 裏面的改變須要自定義元素或者手動爲 shadow 根目錄添加一個 MutationObserver。若是你歷來沒有據說過 shadow DOM,你能夠在 here 查看更多。
最後,他們提供了略有不一樣的掛鉤。自定義元素有 adoptedCallback
鉤子,然而 MutationObservers
能夠在任意層次監聽文本的改變和子元素的改變。
綜上所述,把這二者的最好的方面結合起來使用是一個好主意。
由於自定義元素尚未被普遍支持,因此必須使用 MutationObservers 來檢測 DOM 改變。主要有兩種選擇。
我選擇後者,由於 MutationObservers 是檢測子元素改變的必要條件,即便在徹底支持自定義元素的瀏覽器中也是如此。
我爲下一版本的 NX 使用的系統簡單地在舊瀏覽器的文檔添加一個 MutationObserver。然而在現代瀏覽器中,該系統使用自定義元素爲最頂層的組件建立鉤子,而且在他們的 connectedCallback
鉤子中添加一個 MutationObserver
。這個 MutationObserver 能夠用來扮演在組件內部檢測進一步的 DOM 變化的角色。
它只查找文檔中由框架控制的部分中的更改。對應的代碼大概以下。
function registerRoot (name) {
if ('customElements' in window) {
registerRootV1(name)
} else if ('registerElement' in document) {
registerRootV0(name)
} else {
// add a MutationObserver to the document
}
}
function registerRootV1 (name) {
function RootElement () {
return Reflect.construct(HTMLElement, [], RootElement)
}
const proto = RootElement.prototype
Object.setPrototypeOf(proto, HTMLElement.prototype)
Object.setPrototypeOf(RootElement, HTMLElement)
proto.connectedCallback = connectedCallback
proto.disconnectedCallback = disconnectedCallback
customElements.define(name, RootElement)
}
function registerRootV0 (name) {
const proto = Object.create(HTMLElement)
proto.attachedCallback = connectedCallback
proto.detachedCallback = disconnectedCallback
document.registerElement(name, { prototype: proto })
}
function connectedCallback (elem) {
// add a MutationObserver to the root element
}
function disconnectedCallback (elem) {
// remove the MutationObserver from the root element
}
複製代碼
這會爲現代瀏覽器帶來性能的好處,由於他們只需處理極少的 DOM 變化。
總而言之,重構NX能夠很容易地使用沒有很大性能影響的自定義元素,可是自定義元素在某些狀況仍然會帶來性能的提高。我須要從他們之中獲得的有用的乾貨便是一個靈活的底層 API 和大量的同步生命週期鉤子。