無標題文檔

Mini,又个 Javascript 选择器

Javascript 选择器(selector engine)似乎从 jQuery 流行以来就大行其道,改变了原有 Javascript 选择 DOM 节点的方式。

目前 Javascript 选择器也有众多的选择,包括能列举出来的就有 PeppySizzle 以及 Sly 等,它们都实现了所有的 CSS3 选择器 并且性能不俗。

https://friable.rocks/_/2009_11_05/241818145cd9.jpg

而此次特别要推荐 Mini 的因素有很多。其最大的亮点就是从实用主义出发,简单高效的完成任务。

实用主义

jQuery 的作者 John Resig 曾经统计 jQuery 框架常用的几个选择器 。 很惊讶的发现用户其实常用 tagName、className 以及 id 就能完成 95% 以上的工作。

而 Mini 就从实用出发,它并没有标榜自己实现了全部 CSS3 的所有选择器,只是实现了下面的选择器及其变种:

是的,虽然不多,但是相信在日常中已经足够使用。

https://friable.rocks/_/2009_11_05/601198145f37.jpg

实用主义虽然让 Mini 提供的功能有限,但从个侧面带来的优势就是 性能方面非常的理想 。 当然这还有与它的内部实现有关。

简单至上

Mini 的代码很简单,甚至不用恐惧于如何去读懂 Sizzle 这样的每行源代码。先从它的调用函数 _find 看起,往下阅读几行,你就发现它性能的秘密

if (!simple && context.querySelectorAll) {
    return realArray(context.querySelectorAll(selector));
}

很多的现代浏览器(包括 Gecko、WebKit 等)都 提供了原生的 Javascript 选择器支持 。 Mini 充分得利用了这点,从而将大部分的工作交给了浏览器(也免除了日后的维护之忧)。这点非常值得称道, 也是我们以后写 Javascript 应该走的方向 -- 要基于浏览器提供的功能而非类型进行对应的开发。

_find 其后的逻辑非常的简单,使用几个正则将提供的选择器解析出来,分别根据 getElementsByClassName、 getElementsByClassName 以及 getElementById 获取节点。

继续根据上面的思路,由于陈旧的 IE 没有既没有提供 querySelectorAll 也没有提供 getElementsByClassName, 那么它只能走到 else 的尽头,使用 filterByAttr 函数。

取到对应 className 以及 tagName 后,还得根据选择器获取其与其相符的节点,那么就轮到 filterParents 干活了。

filterParents 这个函数也是相对 _find 比较核心的函数,而且也是主要的性能消耗点。filterParents 其实要做的和 _find 类似,唯一不同的是它根据关系符向上剔除未满足条件的节点。

通过上面两个函数所得到的节点,通常已经是满足选择符条件的节点了。 但是可能的情况就是有重复的节点,这时就该 unique 函数上场,顾名思义剔除重复的节点。

这里要提到的就是 unique 函数做了个简单的 memoization 实现 , 作者的用心可谓从细节方面体现得淋漓尽致。还有个小技巧就是

var uid = +new Date();

竟然可以用这样直接返回时间戳,俏皮的代码让人再次感到犹如嚼了口薄荷糖,非常的清新(好吧,我火星了)。

读码到最后,有一点思考就是为何每次都要使用 realArray 函数强制将 NodeList 转换成 Array?虽然使用 querySelectorAll 返回的是 NodeList ,但是也是可以使用 Array 操作迭代。作者这样的做法我推测,可能是出于返回数据类型一致的缘故。

观点

说说到我的个人观点,这可能会让这篇文章前后矛盾。Javascript 选择器(selector engine)我个人不是非常推荐使用。

原因主要有两点,其一就是性能,其二就是会让写出来的代码过于的依赖于 DOM 结构。但能读到如 Mini 这样的代码,窥其内部运作机制,未尝不是件非常让人愉悦的事情。

http://james.padolsey.com/wp-content/uploads/me.jpg

最后,说说 Mini 的作者 James Padolsey 。我本人也很难相信,他竟然只有 19 岁!更难 得的是在 他 Blog 上的 About me 页面 中写道

What services do I currently offer?
- Everything JavaScript!

同时, 他也是 jQuery Cookbook 的作者 。 「小小年纪」能够有这样的心态以及成就,让我们这帮「前端老古董」感到唏嘘不已 :^)

Bang 兄 已对 Mini 的代码做了更为详尽的解析,有兴趣的可以参看 他的相关 Blog

innerHTML 的些摘记

异步 innerHTML

innerHTML 插入节点的性能的问题,通常是我们最关注的。 在回答这问题时James Padolsey 给出了他的解决方案 ,看到上述代码不仅赞叹了下:

function asyncInnerHTML(HTML, callback) {
    var temp = document.createElement('div'),
        frag = document.createDocumentFragment();
    temp.innerHTML = HTML;
    (function(){
        if(temp.firstChild) {
            frag.appendChild(temp.firstChild);
            setTimeout(arguments.callee, 0);
        } else {
            callback(frag);
        }
    })();
}
  1. 充分利用闭包解决 IE6 的内存溢出问题
  2. 使用 延时 0 将操作从队列中拉出 ,防止浏览器假死
  3. Document Fragment 给予我们个相当好的沙盘,只是我们经常忘记了它
  4. 回调的节点可以使用 DOM 标准的手法(appendChild)插入

了解了参数就很容易调用,例如

var htmlStr = '<div><p>...</p><p>...</p><div><div>...</div>';
asyncInnerHTML(htmlStr, function(fragment){
    document.body.appendChild(fragment);
});

再次不禁赞叹下!

组织 innerHTML 字符串

说到 innerHTML ,通常在这操作之前会有大部分的字符串操作用于连接节点。考虑下面的三种做法,有何不同

方式一

var arr = ['item 1', 'item 2', 'item 3', ...];
for (var i = 0, l = arr.length, list = ''; i < l; i++) {
    list += '<li>' + arr[i] + '</li>';
}
list = '<ul>' + list + '</ul>';

方式二

var arr = ['item 1', 'item 2', 'item 3', ...];
for (var i = 0, l = arr.length, list = []; i < l; i++) {
    list[list.length] = '<li>' + arr[i] + '</li>';
}
list = '<ul>' + list.join('') + '</ul>';

方式三

var arr = ['item 1', 'item 2', 'item 3', ...];
var list = '<ul><li>' + arr.join('</li><li>') + '</li></ul>';

详细的对比 测试在这里 (没错,还是 James Padolsey 那小子的 Blog)。同时, PPK 也整理了份有关 innerHTML 的速度测试报告

IE 的陷阱

对于 IE,innerHTML 有个不大不小的陷阱( via ),就是在 tbody 中插入 innerHTML 时,会报莫名的「未知的运行错误」。

测试地址在这里 (经过测试,在 IE8 中仍然如此)。有兴趣的同学可以 参看更详细的信息

IE8 的 JSON 解析 Bug

使用 IE8 时发现其 原生的 JSON 解析器 存在 Bug,让我们先用 IE8 打开 DEMO 页面体验下。

http://friable.rocks/bug/ie8-json-stringify.html

事件检测

给浏览器绑定事件有时候是非常痛苦的事情,不同的浏览器提供不尽相同的功能的同时,也提供了不同的事件。例如,IE 系列的浏览器支持 mouseenter/mouseleave 事件;Opera 不支持 contextmenu 以及 input 相关的 onbeforepaste、onbeforecut 等事件。

浏览器之间不同程度的事件支持情况,会加重编写跨浏览器的开发成本。而在某种「无奈」的情况下(特别针对 IE),我们都会采用 浏览器嗅探 来决定绑定不同的事件,例如绑定鼠标滚轮事件:

if (Env.ua.ie) {
    Event.on(document, 'mousewheel', callback);
} else {
    Event.on(document, 'DOMMouseScroll', callback);
}

或者另外中做法就是干脆绑定两个事件。虽然这也能很正常的工作,但我们都了解,无论是基于浏览器嗅探还是重复绑定无用的事件,这都是不完美的解决方案。

Juriy Zaytsev 的代码 给了我们解决这个问题的些启示,他利用 DOM 的特性,来判断针对某元素是否支持具体事件。例如

var el = document.createElement('div');

el.setAttribute('onclick', 'return;');
typeof el.onclick; // "function"

el.setAttribute('onclick2', 'return;');
typeof el.onclick2; // "undefined"

那么,根据这一特性,就可以编写出不依赖浏览器嗅探的事件检测脚本,原文的最终例子

var isEventSupported = (function() {
    // 根据特有的事件创建对应的 HTML 元素
    var TAGNAMES = {
        'select':'input','change':'input',
        'submit':'form','reset':'form',
        'error':'img','load':'img','abort':'img'
    }

    function isEventSupported(eventName) {
        var el = document.createElement(TAGNAMES[eventName] || 'div');
        eventName = 'on' + eventName;

        // 检测元素是否已经包含了对应的事件
        var isSupported = (eventName in el);

        // 如果没有对应事件,则尝试增加对应事件,然后判断是否为回调
        if (!isSupported) {
            el.setAttribute(eventName, 'return;');
            isSupported = typeof el[eventName] == 'function';
        }
        el = null;
        return isSupported;
    }
    return isEventSupported;
})();

那么,判断是否为 Opera 浏览器使用

isEventSupported("contextmenu")

就好过

navigator.userAgent.indexOf('Opera') > -1 

而且,以后如果 Opera「修复」了该问题,由于 isEventSupported("contextmenu") 返回的是 true,从而也可保证代码健壮的运行。

最后,原文作者写了个 简单的测试页面 ,用于检测具体浏览器的 DOM Level2 的支持情况。

-- Split --

需要了解其他利用 DOM 特性的小窍门,请 查看这里

发布名为 Motion 的动画组件

也写了几年的代码了,越发的感觉到「写代码容易、养代码难」。当代码多了以后,总会有种想法将有用的代码封装起来,方便更多人也避免大家重复造轮子。 于是,就有了这个动画组件

http://friable.rocks/motion/accessory/logo-motion-200.png

下面是复制粘贴时间,先回答下大家可能要问到的些问题。

又多了款这样(类似)的脚本,让我如何选择?

没有最好的,只有更适合的。或许其他同类型的脚本也能很好的 完成工作(包括很多库中的动画组件能够完成更好的效果)。

本人编写该组件的目的,除了能让您能高效率完成动画效果的 同时,也希望您能了解动画组件的原理。

而且完成这些复杂效果只需要 3KB (压缩后)并且可以定制,何乐而不为 :^)

该动画组件支持哪些浏览器?

经过本人的测试,主流的浏览器普遍都被支持。其中包括 Internet Explorer(6/7/8) 、最新版本的 Firefox、Safari、Chrome 以及 Opera,其他支持 Javascript 的浏览器也应该可被支持。

欢迎您提供相应的其他浏览器的兼容情况。

-- Split --

其实编写这个组件并非偶然,在过年的时候 就曾经发布过类似的脚本 ,这次的发布也仅仅是做了些优化。我第一次感觉到发布组件的压力是如此的巨大,甚至我亲自为其每个方法都编写了测试用例以及详细的文档和例子。

好了废话不多说,接下来的一步就是逐渐的优化代码和增强功能,兄弟们如果有好的意见和建议,欢迎不吝提出。

我的照片

嗨!我叫「明城」,八零后、码农、宁波佬,现居杭州。除了这里,同时也欢迎您关注我的 GitHub (2) 、 TwitterInstagram 等。

这个 Blog 原先的名字叫 Gracecode.com 、现在叫 「無標題文檔」 。 其实无所谓叫什么名字,作为码农知道取名是件很难的事情。最后想到的这个名字,其实都没啥特别的含义,系统默认的文件名而已。

作为八零后,自认为还仅存点傲娇式的幽默感,以及对平淡生活的追求和向往。 为了免得对号入座和不必要的麻烦,声明本站点所持观点仅代表个人意见,不代表自己所服务公司的立场。

如果您想联系我,可以发我邮件 `echo bWluZ2NoZW5nQG91dGxvb2suY29tCg== | base64 -d`

文章

项目