目前,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