溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何寫一個更好的Javascript DOM庫

發(fā)布時間:2021-11-17 14:32:14 來源:億速云 閱讀:114 作者:柒染 欄目:web開發(fā)

如何寫一個更好的Javascript DOM庫,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。

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

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

后來, Daniel Buchner創(chuàng)造了SelectorListener,于是有了“l(fā)ive擴展(live extensions)”的概念。我開始考慮創(chuàng)造一系列的函數(shù),使得我們可以使用比迄今為止用過的方法都更好的方式來創(chuàng)建非干擾性的DOM組件。目標是回顧已有的API與解決方案,并創(chuàng)造一個更干凈、可測試且輕量級的庫。

向庫添加有用的特性

是live擴展的想法鼓勵我開發(fā)了better-dom項目,不過,還有一些其他的有趣的特性使得它成為一個獨特的庫。我們快速地看一下:

  • live擴展

  • 原生的動畫

  • 內(nèi)置的微模板

  • 國際化的支持

live擴展

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

目標是,只需定義一次擴展并使得所有未來的元素快速略過初始化函數(shù),而無論部件的復雜度。這個很重要,因為它使得我們可以聲明式地開發(fā)web頁面,從而在AJAX應用中表現(xiàn)優(yōu)異。

Live擴展使得你無需調(diào)用初始化方法就可以操作未來的元素

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

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 元素的樣式:

.custom-title {   position: fixed; /* required */   border: 1px solid #faebcc;   background: #faf8f0; }

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

live擴展是獨立的;這樣它們并不需要為了使得未來的內(nèi)容生效去調(diào)用一個初始化方法。因此它們可以與任何DOM庫結(jié)合使用,將UI代碼分割成許多小的獨立的塊,從而簡化應用的邏輯。

***,同樣很重要的,一些關(guān)于Web組件的內(nèi)容。規(guī)范的一部分,“裝飾器” ,意在解決類似的問題。目前,它使用了一種基于標記的實現(xiàn),通過特殊的語法,將事件監(jiān)聽者綁定到子元素上。但它仍只是早期的草案:

“裝飾器,與Web組件的其它部分不同的是,它還沒有一個規(guī)范?!?/p>

原生動畫

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

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

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

.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() 在內(nèi)部將 aria-hidden 屬性值設(shè)置為false或true。這使得CSS可以處理動畫與轉(zhuǎn)換。

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

內(nèi)置的微模板

HTML字符串冗長而繁瑣。尋找替代的過程中我發(fā)現(xiàn)了超棒的Emmet。如今Emmet已經(jīng)是一個非常流行的文本編輯器插件,它擁有漂亮而簡潔的語法。比如這段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表達式。縮寫解析器很快,所以不用擔心付出性能代價。如果需要,還有一個模板預編譯函數(shù)可用。

國際化支持

開發(fā)一個UI組件經(jīng)常會需要本地化&mdash;&mdash;這并不輕松。多年來,很多人使用不同的方法解決這個問題。在better-dom中,我相信改變CSS選擇器的狀態(tài),就如同轉(zhuǎn)換語言。

從概念上說,轉(zhuǎn)換語言正是改變內(nèi)容的“表現(xiàn)”。在CSS2中,有幾個偽類選擇器可用于描述這樣一個模型::lang 以及:before。我們來看下邊的代碼:

[data-i18n="hello"]:before {   content: "Hello Maksim!"; }   [data-i18n="hello"]:lang(ru):before {   content: "Привет Максим!"; }

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

[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

還可以使用參數(shù)化的字符串。直接向關(guān)鍵字符串中添加${param} 變量:

label.i18n("Hello ${user}!", {user: "Maksim"}); // the label will display "Hello Maksim!"

讓原生的APIs 更加優(yōu)雅

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

  • getter 及setter

  • 事件處理

  • 功能性的方法支持

GETTER 及SETTER

原生的 DOM 元素有attributes 及properties的概念,但他們的行為并不完全一致。假設(shè)我們在一個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。這些區(qū)別有時會導致混淆。

在實際使用中,很難想像一個這樣的區(qū)別有用的場景。除此之外,開發(fā)者必須總是牢記哪些值(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)將觸發(fā)對應的attibute(原生行為)的更新。

改良的事件處理

事件處理是DOM中很重要的一部分,然而,我發(fā)現(xiàn)一個根本性的問題:將event對象傳入元素監(jiān)聽者的行為導致關(guān)心可測試性的開發(fā)者不得不偽造***個參數(shù)(事件對象),或是創(chuàng)建一個額外的函數(shù)來傳入事件處理函數(shù)僅需的事件屬性。

var button = document.getElementById("foo");   button.addEventListener("click", function(e) {   handleButtonClick(e.button); }, false);

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

var button = DOM.find("#foo");   button.on("click", handleButtonClick, ["button"]);

默認地,事件處理函數(shù)會被傳入["target", "defaultPrevented"] 數(shù)組,所以不用為了獲得這些屬性添加***一個參數(shù)。

button.on("click", function(target, canceled) {     // handle button click here  });

延時綁定也得到了支持(我建議讀一下Peter Michaux關(guān)于這個主題的回顧)。它是W3C的標準中常規(guī)事件綁定的更加靈活的替換物。它在你需要頻繁進行啟用和關(guān)閉方法調(diào)用時非常有用。

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不提供任何對于遺留的或不同瀏覽器中表現(xiàn)不一致的API的調(diào)用,比如click(), focus() 和submit()。 調(diào)用他們的唯一方式是使用fire 方法,它在沒有監(jiān)聽者返回false的情況下執(zhí)行默認行為:

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規(guī)范了一些的有用的數(shù)組方法,包括 map, filter 以及some。它們允許我們以符合標準的方式使用通用的集合操作。因此現(xiàn)在我們有了諸如Underscore 和Lo-Dash這樣的項目,它們在老的瀏覽器上實現(xiàn)這些方法。

better-dom中的每個元素(或集合)都內(nèi)置了如下的方法:

each (它與 forEach 的區(qū)別在于返回this 而不是 undefined)

some

every

map

filter

reduce[Right]

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的問題

在不放棄向后兼容的情況下,以下的絕大多數(shù)問題無法在jQuery中得到解決。這是為什么創(chuàng)造一個新的庫看起來是合乎邏輯的解決途徑。

  • “神奇的” $ 函數(shù)

  • [] 操作符的值

  • 關(guān)于 return false的問題

  • find 以及findAll

“神奇的” $ 函數(shù)

每個人都或多或少聽說過$ (美元) 函數(shù)的神奇。一個單字符的名字并不具有描述性,所以它看起來像是一個內(nèi)置的語言操作符。這也正是缺乏經(jīng)驗的開發(fā)者的代碼中$的調(diào)用隨處可見的原因。

在背后的實現(xiàn)中,$是個極其復雜的函數(shù)。經(jīng)常地執(zhí)行,尤其是 mousemove 、scroll這樣的頻繁事件中,會導致較差的UI性能。

盡管有很多文章建議將jQuery對象緩存下來,開發(fā)者依舊在將$大量嵌入在代碼中,因為jQuery庫的語法鼓勵了這樣的代碼風格。

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

$("a"); // => searches all elements that match “a” selector $("<a>"); // => creates a <a> element with jQuery wrapper

better-dom 使用了幾個函數(shù)來承擔jQuery中$函數(shù)的職責:find[All] 以及DOM.create。find[All]  被用來依據(jù)CSS選擇器來獲取元素。 DOM.create 在內(nèi)存中創(chuàng)建一個新的節(jié)點樹。它們的名字就可以清晰地表明它們的職責。

[]操作符的值

導致$函數(shù)被頻繁調(diào)用的另一個原因正是方括號操作符。當一個新的jQuery對象被創(chuàng)建的時候,所有相關(guān)的節(jié)點都被存儲在數(shù)值型屬性中。但是請注意,這樣一個數(shù)值屬性的值包含了一個原生的元素實例(而非經(jīng)jQuery包裝過的對象):

var links = $("a");   links[0].on("click", function() { ... }); // throws an error $(links[0]).on("click", function() { ... }); // works fine

正因為這樣的特性,jQuery或是其它庫(比如Underscore)中的每一個功能方法都要求當前元素在回調(diào)函數(shù)中使用$()  包起來。因此,開發(fā)者必須時刻牢記他們正在操作的對象類型&mdash;&mdash;一個原生元素或是一個包裝過的對象&mdash;&mdash;盡管事實上他們正在使用一個操作DOM的庫。

在better-dom中,方括號操作符返回一個庫對象,所以開發(fā)者可以忘記原生元素。只有一種可接受的方式來獲取原生元素:使用一個特別的 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)。

關(guān)于RETURN FALSE的問題

jQuery事件處理函數(shù)中返回false后的奇怪的攔截行為讓我一直很糾結(jié)。依據(jù)W3C的標準,它應該在大多數(shù)情況下取消默認行為。在jQuery中,return false 還會阻止事件代理。

這樣的捕獲會導致問題:

1 自行調(diào)用stopPropagation() 可能導致兼容性問題,因為它阻止了其他任務相關(guān)的監(jiān)聽者的執(zhí)行。

2 大部分開發(fā)者(即使是一些有經(jīng)驗的)并沒有意識到這樣的行為

尚不清楚為什么jQuery社區(qū)決定不遵循標準。但better-dom并不會重蹈覆轍。 所以,正如每個人所預期的,事件句柄中的return false 只會阻止瀏覽器默認行為,而不會干擾事件冒泡。

FIND 以及FINDALL

元素查找是在瀏覽器中代價***的操作之一。兩個原生的方法可以用來實現(xiàn)它:querySelector以及querySelectorAll。區(qū)別在于前者在匹配到***個結(jié)果后即停止查找。

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

jQuery提供了一個find 方法,使用querySelectorAll ,用于一般的情形。目前還沒有函數(shù)使用querySelector 來只獲取***個匹配的元素。

better-dom 庫有兩個單獨的方法:find 及findAll。它們允許我們使用querySelector 優(yōu)化。為了評估潛在的性能提升,我在我上一個商業(yè)項目的所有源代碼中搜索了這些方法的使用:

find
在11個文件中匹配103次
findAll
在4個文件中匹配14次

很明顯find 方法要受歡迎得多。這說明querySelector 優(yōu)化在大多數(shù)情況下是有意義的,并能推動相當?shù)男阅芴嵘?/p>

結(jié)論

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

我在開發(fā)過程中學習到的一件事是,如果你不喜歡某個標準,或者你對該如何做某件事情有自己不同看法,那么就去實現(xiàn)它,證明你的方法可行。

看完上述內(nèi)容,你們掌握如何寫一個更好的Javascript DOM庫的方法了嗎?如果還想學到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI