寫一個更好的Javascript DOM庫

jobbole發表於2014-02-14

目前,jQuery是事實上的操作文件物件模型(DOM)的庫。它可以與流行的客戶端MV*框架結合使用,並且擁有大量的外掛與大型的社群。開發者對於Javascript的興趣與日俱增的同時,很多人開始好奇,原生的API是如何工作的,以及我們何時應該直接使用它們而不是引用一個額外的庫。

最近,我開始發現越來越多的jQuery的問題,至少是在我的使用中是這樣的。其中的絕大多數涉及到jQuery的核心,在不取消向後相容的情況下無法解決——而向後相容又非常重要。與很多人一樣,我繼續使用了它一段時間,每天瀏覽所有討厭的瀏覽器怪異模式。

後來, Daniel Buchner創造了SelectorListener,於是有了“live擴充套件(live extensions)”的概念。我開始考慮創造一系列的函式,使得我們可以使用比迄今為止用過的方法都更好的方式來建立非干擾性的DOM元件。目標是回顧已有的API與解決方案,並創造一個更乾淨、可測試且輕量級的庫。

向庫新增有用的特性

是live擴充套件的想法鼓勵我開發了better-dom專案,不過,還有一些其他的有趣的特性使得它成為一個獨特的庫。我們快速地看一下:

  • live擴充套件
  • 原生的動畫
  • 內建的微模板
  • 國際化的支援

live擴充套件

jQuery有一個叫做“live事件(live events)”的概念。藉助事件代理,它使得開發者可以處理現有的以及未來的元素。但是許多情況會要求更大的靈活度。比如為了初始化一個部件而需要對DOM進行轉換,事件代理就會力不從心。故而,live擴充套件。

目標是,只需定義一次擴充套件並使得所有未來的元素快速略過初始化函式,而無論部件的複雜度。這個很重要,因為它使得我們可以宣告式地開發web頁面,從而在AJAX應用中表現優異。

new-feature-with-jquery
Live擴充套件使得你無需呼叫初始化方法就可以操作未來的元素

我們來看一個簡單的例子。假設我們的任務是實現一個完全自定義的提示框。:hover 偽類選擇器並無幫助,因為提示框的位置隨著滑鼠移動而變化。事件代理也不是很合適;監聽文件樹中所有元素的mouseover 及mouseleave 事件代價很大。live擴充套件將拯救你!

我們可以在CSS中定義 .custom-title 元素的樣式:

當你向頁面中插入一個帶title 屬性的新元素時,最有趣的部分發生了。自定義的提示框無需呼叫任何初始化方法即可生效。

live擴充套件是獨立的;這樣它們並不需要為了使得未來的內容生效去呼叫一個初始化方法。因此它們可以與任何DOM庫結合使用,將UI程式碼分割成許多小的獨立的塊,從而簡化應用的邏輯。

最後,同樣很重要的,一些關於Web元件的內容。規範的一部分,“裝飾器” ,意在解決類似的問題。目前,它使用了一種基於標記的實現,通過特殊的語法,將事件監聽者繫結到子元素上。但它仍只是早期的草案:

“裝飾器,與Web元件的其它部分不同的是,它還沒有一個規範。”

原生動畫

多虧了 Apple, CSS現在擁有了對動畫的良好支援。過去動畫通常使用Javascript的setInterval 及setTimeout實現。這曾經是很酷的特性——但是現在看來,它更像是壞的實踐。原生的動畫總是更平滑:常常更快,開銷更小,並且在瀏覽器不支援的情況下可以很好地降級。

better-dom中,沒有animate方法:只有show, hide 以及toggle。庫使用基於標準的aria-hidden屬性來在CSS中獲取一個隱藏元素的狀態。

為了說明它是如何工作的,我們來為先前介紹的提示框新增一個簡單的動畫效果:

show() 以及hide() 在內部將 aria-hidden 屬性值設定為false或true。這使得CSS可以處理動畫與轉換。

你可以在這個demo中看到更多使用了better-dom的動畫。

內建的微模板

HTML字串冗長而繁瑣。尋找替代的過程中我發現了超棒的Emmet。如今Emmet已經是一個非常流行的文字編輯器外掛,它擁有漂亮而簡潔的語法。比如這段HTML:

與對應的微模板比較:

在better-dom中,任何接受HTML的方法同樣接受Emmet表示式。縮寫解析器很快,所以不用擔心付出效能代價。如果需要,還有一個模板預編譯函式可用。

國際化支援

開發一個UI元件經常會需要本地化——這並不輕鬆。多年來,很多人使用不同的方法解決這個問題。在better-dom中,我相信改變CSS選擇器的狀態,就如同轉換語言。

從概念上說,轉換語言正是改變內容的“表現”。在CSS2中,有幾個偽類選擇器可用於描述這樣一個模型::lang 以及:before。我們來看下邊的程式碼:

這是個很簡單的把戲:html 元素的lang 屬性控制當前語言,而content 屬性值根據當前的語言變化。通過使用如data-i18n這樣的屬性,我們可以在HTML中維護文字內容。

當然,這樣的CSS並不吸引人,所以better-com提供了兩個幫助方法:i18n 及DOM.importStrings。前者用於更新 data-i18n 屬性為合適的值,後者為特定的語言本地化字串。

還可以使用引數化的字串。直接向關鍵字串中新增${param} 變數:

 

讓原生的APIs 更加優雅

 

通常我們都希望遵從標準。但是有時候標準對使用者並不友好。DOM就是一團糟 ,為了將其變得好用,我們不得不把它包裝到一個方便的API中。儘管開源的庫已經作了很多改進,仍有一些部分可以做得更好:

  • getter 及setter
  • 事件處理
  • 功能性的方法支援

GETTER 及SETTER

原生的 DOM 元素有attributes 及properties的概念,但他們的行為並不完全一致。假設我們在一個web頁面中有如下的標記:

為了解釋為什麼“DOM就是一團糟”,我們來看這:

一個attribute與其在HTML中對應的字串相等,但元素的同名property可能會有一些奇怪的行為,比如在上邊列出來的,生成完全合格的URL。這些區別有時會導致混淆。

在實際使用中,很難想像一個這樣的區別有用的場景。除此之外,開發者必須總是牢記哪些值(attribute 或property)被使用了,這會引入沒必要的複雜度。

在better-dom中,事情要清楚得多。每個元素都只有智慧的getter與setter。

首先,它做一次屬性(property)查詢,如果是已定義的,則返回供操作。不然,getter 及setter 作用於對應的元素屬性(attribute)。對於boolean值(checked, selected, 這些), 可以直接使用 true 或 false 來更新值:改變元素的該屬性(property)將觸發對應的attibute(原生行為)的更新。

改良的事件處理

事件處理是DOM中很重要的一部分,然而,我發現一個根本性的問題:將event物件傳入元素監聽者的行為導致關心可測試性的開發者不得不偽造第一個引數(事件物件),或是建立一個額外的函式來傳入事件處理函式僅需的事件屬性。

這很煩人。不過如果我們將可變部分抽象為一個引數,我們就可以擺脫額外的函式:

預設地,事件處理函式會被傳入[“target”, “defaultPrevented”] 陣列,所以不用為了獲得這些屬性新增最後一個引數。

延時繫結也得到了支援(我建議讀一下Peter Michaux關於這個主題的回顧)。它是W3C的標準中常規事件繫結的更加靈活的替換物。它在你需要頻繁進行啟用和關閉方法呼叫時非常有用。

最後,同樣很重要的,better-dom不提供任何對於遺留的或不同瀏覽器中表現不一致的API的呼叫,比如click(), focus() 和submit()。 呼叫他們的唯一方式是使用fire 方法,它在沒有監聽者返回false的情況下執行預設行為:

功能性方法的支援

ES5規範了一些的有用的陣列方法,包括 map, filter 以及some。它們允許我們以符合標準的方式使用通用的集合操作。因此現在我們有了諸如UnderscoreLo-Dash這樣的專案,它們在老的瀏覽器上實現這些方法。

better-dom中的每個元素(或集合)都內建瞭如下的方法:

each (它與 forEach 的區別在於返回this 而不是 undefined)
some
every
map
filter
reduce[Right]

避免jQuery的問題

在不放棄向後相容的情況下,以下的絕大多數問題無法在jQuery中得到解決。這是為什麼創造一個新的庫看起來是合乎邏輯的解決途徑。

  • “神奇的” $ 函式
  • [] 操作符的值
  • 關於 return false的問題
  • find 以及findAll

“神奇的” $ 函式

每個人都或多或少聽說過$ (美元) 函式的神奇。一個單字元的名字並不具有描述性,所以它看起來像是一個內建的語言操作符。這也正是缺乏經驗的開發者的程式碼中$的呼叫隨處可見的原因。

在背後的實現中,$是個極其複雜的函式。經常地執行,尤其是 mousemove 、scroll這樣的頻繁事件中,會導致較差的UI效能。

儘管有很多文章建議將jQuery物件快取下來,開發者依舊在將$大量嵌入在程式碼中,因為jQuery庫的語法鼓勵了這樣的程式碼風格。

$函式的另一個問題是,它可以被用來做完全不同的兩件事。人們已經喜歡了這樣的語法,但通常來說,這是一個失敗的函式設計:

better-dom 使用了幾個函式來承擔jQuery中$函式的職責:find[All] 以及DOM.create。find[All] 被用來依據CSS選擇器來獲取元素。 DOM.create 在記憶體中建立一個新的節點樹。它們的名字就可以清晰地表明它們的職責。

[]操作符的值

導致$函式被頻繁呼叫的另一個原因正是方括號操作符。當一個新的jQuery物件被建立的時候,所有相關的節點都被儲存在數值型屬性中。但是請注意,這樣一個數值屬性的值包含了一個原生的元素例項(而非經jQuery包裝過的物件):

正因為這樣的特性,jQuery或是其它庫(比如Underscore)中的每一個功能方法都要求當前元素在回撥函式中使用$() 包起來。因此,開發者必須時刻牢記他們正在操作的物件型別——一個原生元素或是一個包裝過的物件——儘管事實上他們正在使用一個操作DOM的庫。

在better-dom中,方括號操作符返回一個庫物件,所以開發者可以忘記原生元素。只有一種可接受的方式來獲取原生元素:使用一個特別的 legacy方法。

事實上,只有非常少見的情況會需要這個方法,比如相容一個原生的方法,或是另一個DOM庫(比如上邊例子中的Hammer)。

關於RETURN FALSE的問題

jQuery事件處理函式中返回false後的奇怪的攔截行為讓我一直很糾結。依據W3C的標準,它應該在大多數情況下取消預設行為。在jQuery中,return false 還會阻止事件代理。

這樣的捕獲會導致問題:

1 自行呼叫stopPropagation() 可能導致相容性問題,因為它阻止了其他任務相關的監聽者的執行。

2 大部分開發者(即使是一些有經驗的)並沒有意識到這樣的行為

尚不清楚為什麼jQuery社群決定不遵循標準。但better-dom並不會重蹈覆轍。 所以,正如每個人所預期的,事件控制程式碼中的return false 只會阻止瀏覽器預設行為,而不會干擾事件冒泡。

FIND 以及FINDALL

元素查詢是在瀏覽器中代價最大的操作之一。兩個原生的方法可以用來實現它:querySelector以及querySelectorAll。區別在於前者在匹配到第一個結果後即停止查詢。

這個特性使得我們可以顯著減少特定情形下的迭代次數。在我的測試中,速度提升到了二十倍!而且,可以預見,依據文件樹的規模,提升可能達到更多。

jQuery提供了一個find 方法,使用querySelectorAll ,用於一般的情形。目前還沒有函式使用querySelector 來只獲取第一個匹配的元素。

better-dom 庫有兩個單獨的方法:find 及findAll。它們允許我們使用querySelector 優化。為了評估潛在的效能提升,我在我上一個商業專案的所有原始碼中搜尋了這些方法的使用:

find
在11個檔案中匹配103次
findAll
在4個檔案中匹配14次

很明顯find 方法要受歡迎得多。這說明querySelector 優化在大多數情況下是有意義的,並能推動相當的效能提升。

結論

live擴充套件確實使得解決前端問題簡單不少。將UI分割為許多小塊可以帶來更加獨立、可維護的解決方案。不過正如我們所展示的,一個框架不僅僅是關於這些(儘管這是主要目標)。

我在開發過程中學習到的一件事是,如果你不喜歡某個標準,或者你對該如何做某件事情有自己不同看法,那麼就去實現它,證明你的方法可行。這也很有趣!

更多關於better-dom 專案的資訊可以在GitHub找到。

 

感謝@陳鑫偉 校對本文;

相關文章