目前,jQuery是事實上的操做文檔對象模型(DOM)的庫。它能夠與流行的客戶端MV*框架結合使用,而且擁有大量的插件與大型的社區。開發者對於Javascript的興趣與日俱增的同時,不少人開始好奇,原生的API是如何工做的,以及咱們什麼時候應該直接使用它們而不是引用一個額外的庫。javascript
最近,我開始發現愈來愈多的jQuery的問題,至少是在個人使用中是這樣的。其中的絕大多數涉及到jQuery的核心,在不取消向後兼容的狀況下沒法解決——而向後兼容又很是重要。與不少人同樣,我繼續使用了它一段時間,天天瀏覽全部討厭的瀏覽器怪異模式。html
後來, Daniel Buchner 創造了 SelectorListener,因而有了「live擴展(live extensions)」的概念。我開始考慮創造一系列的函數,使得咱們可使用比迄今爲止用過的方法都更好的方式來建立非干擾性的DOM組件。目標是回顧已有的API與解決方案,並創造一個更乾淨、可測試且輕量級的庫。前端
是live擴展的想法鼓勵我開發了better-dom項目,不過,還有一些其餘的有趣的特性使得它成爲一個獨特的庫。咱們快速地看一下:java
jQuery有一個叫作「live事件(live events)」的概念。藉助事件代理,它使得開發者能夠處理現有的以及將來的元素。可是許多狀況會要求更大的靈活度。好比爲了初始化一個部件而須要對DOM進行轉換,事件代理就會力不從心。故而,live擴展。node
目標是,只需定義一次擴展並使得全部將來的元素快速略過初始化函數,而不管部件的複雜度。這個很重要,由於它使得咱們能夠聲明式地開發web頁面,從而在AJAX應用中表現優異。jquery
Live擴展使得你無需調用初始化方法就能夠操做將來的元素git
咱們來看一個簡單的例子。假設咱們的任務是實現一個徹底自定義的提示框。:hover 僞類選擇器並沒有幫助,由於提示框的位置隨着鼠標移動而變化。事件代理也不是很合適;監聽文檔樹中全部元素的mouseover 及mouseleave 事件代價很大。live擴展將拯救你!github
DOM.extend("[title]", { constructor: function() { var tooltip = DOM.create("span.custom-title"); // set the title's textContent and hide it initially tooltip.set("textContent", this.get("title")).hide(); this // remove legacy title .set("title", null) // store reference for quicker access .data("tooltip", tooltip) // register event handlers .on("mouseenter", this.onMouseEnter, ["clientX", "clientY"]) .on("mouseleave", this.onMouseLeave) // insert the title element into DOM .append(tooltip); }, onMouseEnter: function(x, y) { this.data("tooltip").style({left: x, top: y}).show(); }, onMouseLeave: function() { this.data("tooltip").hide(); } });
咱們能夠在CSS中定義 .custom-title 元素的樣式:web
.custom-title { position: fixed; /* required */ border: 1px solid #faebcc; background: #faf8f0; }
當你向頁面中插入一個帶title 屬性的新元素時,最有趣的部分發生了。自定義的提示框無需調用任何初始化方法便可生效。segmentfault
live擴展是獨立的;這樣它們並不須要爲了使得將來的內容生效去調用一個初始化方法。所以它們能夠與任何DOM庫結合使用,將UI代碼分割成許多小的獨立的塊,從而簡化應用的邏輯。
最後,一樣很重要的,一些關於Web組件的內容。規範的一部分,「裝飾器」 ,意在解決相似的問題。目前,它使用了一種基於標記的實現,經過特殊的語法,將事件監聽者綁定到子元素上。但它仍只是早期的草案:
「裝飾器,與Web組件的其它部分不一樣的是,它尚未一個規範。」
多虧了 Apple, CSS如今擁有了對動畫的良好支持。過去動畫一般使用Javascript的setInterval 及setTimeout實現。這曾經是很酷的特性——可是如今看來,它更像是壞的實踐。原生的動畫老是更平滑:經常更快,開銷更小,而且在瀏覽器不支持的狀況下能夠很好地降級。
better-dom中,沒有animate方法:只有show, hide 以及toggle。庫使用基於標準的aria-hidden屬性來在CSS中獲取一個隱藏元素的狀態。
爲了說明它是如何工做的,咱們來爲先前介紹的提示框添加一個簡單的動畫效果:
.custom-title { position: fixed; /* required */ border: 1px solid #faebcc; background: #faf8f0; /* animation code */ opacity: 1; -webkit-transition: opacity 0.5s; transition: opacity 0.5s; } .custom-title[aria-hidden=true] { opacity: 0; }
show() 以及hide() 在內部將 aria-hidden 屬性值設置爲false或true。這使得CSS能夠處理動畫與轉換。
你能夠在這個demo中看到更多使用了better-dom的動畫。
HTML字符串冗長而繁瑣。尋找替代的過程當中我發現了超棒的Emmet。現在Emmet已是一個很是流行的文本編輯器插件,它擁有漂亮而簡潔的語法。好比這段HTML:
body.append("<ul><li class='list-item'></li><li class='list-item'></li><li class='list-item'></li></ul>");
與對應的微模板比較:
body.append("ul>li.list-item*3");
在better-dom中,任何接受HTML的方法一樣接受Emmet表達式。縮寫解析器很快,因此不用擔憂付出性能代價。若是須要,還有一個模板預編譯函數可用。
開發一個UI組件常常會須要本地化——這並不輕鬆。多年來,不少人使用不一樣的方法解決這個問題。在better-dom中,我相信改變CSS選擇器的狀態,就如同轉換語言。
從概念上說,轉換語言正是改變內容的「表現」。在CSS2中,有幾個僞類選擇器可用於描述這樣一個模型::lang 以及:before。咱們來看下邊的代碼:
[data-i18n="hello"]:before { content: "Hello Maksim!"; } [data-i18n="hello"]:lang(ru):before { content: "Привет Максим!"; }
這是個很簡單的把戲:html 元素的lang 屬性控制當前語言,而content 屬性值根據當前的語言變化。經過使用如data-i18n這樣的屬性,咱們能夠在HTML中維護文本內容。
[data-i18n]:before { content: attr(data-i18n); } [data-i18n="Hello Maksim!"]:lang(ru):before { content: "Привет Максим!"; }
固然,這樣的CSS並不吸引人,因此better-com提供了兩個幫助方法:i18n 及DOM.importStrings。前者用於更新 data-i18n 屬性爲合適的值,後者爲特定的語言本地化字符串。
label.i18n("Hello Maksim!"); // the label displays "Hello Maksim!" DOM.importStrings("ru", "Hello Maksim!", "Привет Максим!"); // now if the page is set to ru language, // the label will display "Привет Максим!" label.set("lang", "ru"); // now the label will display "Привет Максим!" // despite the web page's language
還可使用參數化的字符串。直接向關鍵字符串中添加${param} 變量:
label.i18n("Hello ${user}!", {user: "Maksim"}); // the label will display "Hello Maksim!"
一般咱們都但願聽從標準。可是有時候標準對用戶並不友好。DOM就是一團糟 ,爲了將其變得好用,咱們不得不把它包裝到一個方便的API中。儘管開源的庫已經做了不少改進,仍有一些部分能夠作得更好:
原生的 DOM 元素有attributes 及properties的概念,但他們的行爲並不徹底一致。假設咱們在一個web頁面中有以下的標記:
<a href="/chemerisuk/better-dom" id="foo" data-test="test">better-dom</a>
爲了解釋爲何「DOM就是一團糟」,咱們來看這:
var link = document.getElementById("foo"); link.href; // => "https://github.com/chemerisuk/better-dom" link.getAttribute("href"); // => "/chemerisuk/better-dom" link["data-test"]; // => undefined link.getAttribute("data-test"); // => "test" link.href = "abc"; link.href; // => "https://github.com/abc" link.getAttribute("href"); // => "abc"
一個attribute與其在HTML中對應的字符串相等,但元素的同名property可能會有一些奇怪的行爲,好比在上邊列出來的,生成徹底合格的URL。這些區別有時會致使混淆。
在實際使用中,很難想像一個這樣的區別有用的場景。除此以外,開發者必須老是牢記哪些值(attribute 或property)被使用了,這會引入不必的複雜度。
在better-dom中,事情要清楚得多。每一個元素都只有智能的getter與setter。
var link = DOM.find("#foo"); link.get("href"); // => "https://github.com/chemerisuk/better-dom" link.set("href", "abc"); link.get("href"); // => "https://github.com/abc" link.get("data-attr"); // => "test"
首先,它作一次屬性(property)查找,若是是已定義的,則返回供操做。否則,getter 及setter 做用於對應的元素屬性(attribute)。對於boolean值(checked, selected, 這些), 能夠直接使用 true 或 false 來更新值:改變元素的該屬性(property)將觸發對應的attibute(原生行爲)的更新。
事件處理是DOM中很重要的一部分,然而,我發現一個根本性的問題:將event對象傳入元素監聽者的行爲致使關心可測試性的開發者不得不僞造第一個參數(事件對象),或是建立一個額外的函數來傳入事件處理函數僅需的事件屬性。
var button = document.getElementById("foo"); button.addEventListener("click", function(e) { handleButtonClick(e.button); }, false);
這很煩人。不過若是咱們將可變部分抽象爲一個參數,咱們就能夠擺脫額外的函數:
var button = DOM.find("#foo"); button.on("click", handleButtonClick, ["button"]);
默認地,事件處理函數會被傳入["target", "defaultPrevented"] 數組,因此不用爲了得到這些屬性添加最後一個參數。
button.on("click", function(target, canceled) { // handle button click here });
延時綁定也獲得了支持(我建議讀一下Peter Michaux關於這個主題的回顧)。它是W3C的標準中常規事件綁定的更加靈活的替換物。它在你須要頻繁進行啓用和關閉方法調用時很是有用。
button._handleButtonClick = function() { alert("click!"); }; button.on("click", "_handleButtonClick"); button.fire("click"); // shows "clicked" message button._handleButtonClick = null; button.fire("click"); // shows nothing
最後,一樣很重要的,better-dom不提供任何對於遺留的或不一樣瀏覽器中表現不一致的API的調用,好比click(), focus() 和submit()。 調用他們的惟一方式是使用fire 方法,它在沒有監聽者返回false的狀況下執行默認行爲:
link.fire("click"); // clicks on the link link.on("click", function() { return false; }); link.fire("click"); // triggers the handler above but doesn't do a click
ES5規範了一些的有用的數組方法,包括 map, filter 以及some。它們容許咱們以符合標準的方式使用通用的集合操做。所以如今咱們有了諸如 Underscore 和 Lo-Dash 這樣的項目,它們在老的瀏覽器上實現這些方法。
better-dom中的每一個元素(或集合)都內置了以下的方法:
var urls, activeLi, linkText; urls = menu.findAll("a").map(function(el) { return el.get("href"); }); activeLi = menu.children().filter(function(el) { return el.hasClass("active"); }); linkText = menu.children().reduce(function(memo, el) { return memo || el.hasClass("active") && el.find("a").get() }, false);
在不放棄向後兼容的狀況下,如下的絕大多數問題沒法在jQuery中獲得解決。這是爲何創造一個新的庫看起來是合乎邏輯的解決途徑。
每一個人都或多或少據說過$ (美圓) 函數的神奇。一個單字符的名字並不具備描述性,因此它看起來像是一個內置的語言操做符。這也正是缺少經驗的開發者的代碼中$的調用隨處可見的緣由。
在背後的實現中,$是個極其複雜的函數。常常地執行,尤爲是 mousemove 、scroll這樣的頻繁事件中,會致使較差的UI性能。
儘管有不少文章建議將jQuery對象緩存下來,開發者依舊在將$大量嵌入在代碼中,由於jQuery庫的語法鼓勵了這樣的代碼風格。
$函數的另外一個問題是,它能夠被用來作徹底不一樣的兩件事。人們已經喜歡了這樣的語法,但一般來講,這是一個失敗的函數設計:
$("a"); // => searches all elements that match 「a」 selector $("<a>"); // => creates a <a> element with jQuery wrapper
better-dom 使用了幾個函數來承擔jQuery中$函數的職責:find[All] 以及DOM.create。find[All] 被用來依據CSS選擇器來獲取元素。 DOM.create 在內存中建立一個新的節點樹。它們的名字就能夠清晰地代表它們的職責。
致使$函數被頻繁調用的另外一個緣由正是方括號操做符。當一個新的jQuery對象被建立的時候,全部相關的節點都被存儲在數值型屬性中。可是請注意,這樣一個數值屬性的值包含了一個原生的元素實例(而非經jQuery包裝過的對象):
var links = $("a"); links[0].on("click", function() { ... }); // throws an error $(links[0]).on("click", function() { ... }); // works fine
正由於這樣的特性,jQuery或是其它庫(好比Underscore)中的每個功能方法都要求當前元素在回調函數中使用$() 包起來。所以,開發者必須時刻牢記他們正在操做的對象類型——一個原生元素或是一個包裝過的對象——儘管事實上他們正在使用一個操做DOM的庫。
在better-dom中,方括號操做符返回一個庫對象,因此開發者能夠忘記原生元素。只有一種可接受的方式來獲取原生元素:使用一個特別的 legacy方法。
var foo = DOM.find("#foo"); foo.legacy(function(node) { // use Hammer library to bind a swipe listener Hammer(node).on("swipe", function(e) { // handle swipe gesture here }); });
事實上,只有很是少見的狀況會須要這個方法,好比兼容一個原生的方法,或是另外一個DOM庫(好比上邊例子中的Hammer)。
jQuery事件處理函數中返回false後的奇怪的攔截行爲讓我一直很糾結。依據W3C的標準,它應該在大多數狀況下取消默認行爲。在jQuery中,return false 還會阻止事件代理。
這樣的捕獲會致使問題:
自行調用stopPropagation() 可能致使兼容性問題,由於它阻止了其餘任務相關的監聽者的執行。
大部分開發者(即便是一些有經驗的)並無意識到這樣的行爲
尚不清楚爲何jQuery社區決定不遵循標準。但better-dom並不會重蹈覆轍。 因此,正如每一個人所預期的,事件句柄中的return false 只會阻止瀏覽器默認行爲,而不會干擾事件冒泡。
元素查找是在瀏覽器中代價最大的操做之一。兩個原生的方法能夠用來實現它:querySelector以及querySelectorAll。區別在於前者在匹配到第一個結果後即中止查找。
這個特性使得咱們能夠顯著減小特定情形下的迭代次數。在個人測試中,速度提高到了二十倍!並且,能夠預見,依據文檔樹的規模,提高可能達到更多。
jQuery提供了一個find 方法,使用querySelectorAll ,用於通常的情形。目前尚未函數使用querySelector 來只獲取第一個匹配的元素。
better-dom 庫有兩個單獨的方法:find 及findAll。它們容許咱們使用querySelector 優化。爲了評估潛在的性能提高,我在我上一個商業項目的全部源代碼中搜索了這些方法的使用:
很明顯find 方法要受歡迎得多。這說明querySelector 優化在大多數狀況下是有意義的,並能推進至關的性能提高。
live擴展確實使得解決前端問題簡單很多。將UI分割爲許多小塊能夠帶來更加獨立、可維護的解決方案。不過正如咱們所展現的,一個框架不只僅是關於這些(儘管這是主要目標)。
我在開發過程當中學習到的一件事是,若是你不喜歡某個標準,或者你對該如何作某件事情有本身不一樣見解,那麼就去實現它,證實你的方法可行。這也頗有趣!
更多關於better-dom 項目的信息能夠在GitHub找到。
感謝@陳鑫偉 校對本文;
原文:Writing A Better JavaScript Library For The DOM
轉載於:伯樂在線 - nighca