关联的 script 标签October 28, 2009

从 James Padolsey 这里 得到个好的点子。

在实际写脚本过程中可能有段 Javascript 和 HTML 非常相关(比如实例化 Slider 等这样组件),那么通常我们会将它紧放到 HTML 的后面。

「传统」的做法需要顾虑的点有很多。因为脚本是立即被执行的,所以要考虑例如调用的组件是否已经声明,以及如果有 Ajax 请求是否会堵死浏览器等等。

下面的代码就是本篇 Blog 提供的另个思路,但愿我看起来不是那么的火星:

<div id="some-div">
    <script type=":contextual">
        alert(this.id); // "some-div" is alerted
    </script>
</div>

原文 作者的想法是改变 script 标签的 this 指向到父节点的 Element,从而关联上下文 HTML 结构。

看它的实现代码:

<script type="text/javascript">
~function() {
    var scripts = document.getElementsByTagName('script'),
        script, i = 0;
    while ((script = scripts[i++])) {
        if (/:contextual$/.test(script.type)) {
            (new Function(script.innerHTML)).call(script.parentNode);
        }
    }
}();
</script>

不过如原作者所说的外,其实还有很多顺带的好处

  1. 将 this 指向关联到父节点,遍历查找 DOM 非常的方便
  2. 相关的 script 标签和 HTML 结合紧密,很清楚就能明白这段脚本需要做什么
  3. 统一调用,可以考虑懒加载
  4. 方便复制粘贴 :^)

当然,上面的代码仅仅是个想法而已,在实际编码中还需谨慎应用。滥用此方法可以预料到的 些问题,比如:

  1. 脚本执行顺序改变
  2. 弄乱作用域,如果你的代码严重依赖 this ,那么将会是个噩梦(当然,这本身也不是个好习惯)
  3. 让不了解此机制的其他开发者迷惑

正如原作者所言,在我们写代码的时候能「Thinking outside the box」,那才是最重要的 :^)

-- EOF --

Mini,又个 Javascript 选择器September 16, 2009

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

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

http://files.gracecode.com/2009_11_05/241818145cd9.jpg

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

实用主义

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

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

  • div
  • .example
  • body div
  • div, p
  • div, p, .example
  • div p
  • div > p
  • div.example
  • ul .example
  • #title
  • h1#title
  • div #title
  • ul.foo > * span

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

http://files.gracecode.com/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 的些摘记May 31, 2009

异步 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 中仍然如此)。有兴趣的同学可以 参看更详细的信息

Yahoo 统计