《JavaScript 模式》读书笔记(8)— DOM和浏览器模式1

时间:2022-07-24
本文章向大家介绍《JavaScript 模式》读书笔记(8)— DOM和浏览器模式1,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

  在本书的前面章节中,我们主要集中关注于核心JavaScript(ECMAScript),而并没有太多关注在浏览器中使用JavaScript的模式。本章将探索一些浏览器特定的模式,因为浏览器是使用JavaScript最为常见的环境。同时也是很多人不喜欢使用JavaScript的原因,他们认为JavaScript只是一种浏览器脚本。考虑到在浏览器中存在很多前后矛盾的主机对象和DOM实现,这种想法也是可以理解的。很明显通过使用一些较好的可以减少客户端脚本负担的实践技巧,可以获益颇多。

  在本章您将看到模式被划分为几类,包含DOM脚本、事件处理、远程脚本、页面载入JavaScript的策略和在产品网站上配置JavaScript的步骤等。

  但是首先,让我们简单的从哲学角度来探索如何处理客户端的脚本。

一、关注分离

在网站应用程序的开发过程中主要关心如下三个内容:

内容(Content):HTML的文档。

表现(Presentation):指定文档外观的CSS样式。

行为(Behavior):处理用户交互和文档各种动态变化的JavaScript。

  将这三部分尽可能的相互独立,可以改进将应用程序交付给大量各种用户终端的效果,图形化的浏览器、文本浏览器、针对残疾用户的辅助技术、移动设备等。关注分离(separation of concerns)也体现了渐进增强(progressive enhancement)的思想,最简单的用户终端可以具有最基本的体现(仅能显示HTML文档),并随着用户终端能力的改进而获取更佳的用户体验。如果浏览器支持CSS,那么用户将可以看到文档更好的表现方式。如果浏览器支持JavaScript,那么该文档更大程度上看起来像一个应用程序,并将获取更多增强用户体验的特性。

  在实际中,关注分离意味着:

  • 通过将css关闭来测试页面是否仍然可用,内容是否依然可读。
  • 将JavaScript关闭来测试页面仍然可以执行其正常功能,所有的链接(不包含href = "#" 的实例)是否能够正常工作,所有的表单可以正常工作并正确提交信息。
  • 使用例如headings和lists这样与以上有意义的HTML元素。

  JavaScript层(行为)应该是不引人注意的,也就是说,JavaScript层应该不会给用户造成不便,例如在不支持JavaScript的浏览器中不会造成网页不可用等问题,JavaScript应该是用来加强网页功能,而不能成为网页正常工作的必须组件。

  常见的用于处理浏览器差异性的技术是特性检测技术(capability detection)。该技术建议不要使用用户代理来嗅探代码路径,而应该在运行环境中检查是否有所需的属性或方法。通常将使用代理嗅探这种方法看作一种反模式。有时候这是不可避免的,但是应该在使用特性检测技术无法获得确定性结论时(或者会导致极大的性能损失时),不得已才使用代理嗅探。

// 反模式
if(navigator.userAgent.indexOf('MSIE') !== -1) {
    document.attachEvent('onclick',console.log);
}

// 比较好的做法
if(document.attachEvent) {
    document.attachEvent('onclick',console.log);
}

// 更具体的做法
if(typeof document.attachEvent !== 'undefined') {
    document.attachEvent('onclick',console.log);
}

  采用关注分离还有助于开发、维护、和升级现有Web应用程序,因为当发生故障时,可以知道去什么地方排错。当是JavaScript发生错误时,无需查看HTML代码和CSS代码来查错。

二、DOM脚本

使用页面的DOM树是客户端JavaScript最常用的任务。这也是头痛的主要原因(JavaScript因此获得一些不好的名声),因为不同的浏览器在DOM方法的实现方面并不一致。这也是为什么使用一个好的JavaScript类库(该类库可以抽象出不同浏览器的区别)可以显著加快开发进度。

  让我们来看看在访问和修改DOM树时推荐的一些模式(主要是出于性能方面考虑)。

DOM访问

dom访问的代价是昂贵的,它是制约JavaScript性能的主要瓶颈。这因为dom通常是独立于JavaScript引擎而实现的。从浏览器的视角看,采用该方法是有意义的,因为有的JavaScript应用程序可能根本就不需要DOM。而且除JavaScript以外的其他程序(例如IE中的VBScript)也可以用来和页面的DOM共同工作。

  总之DOM的访问应该减少到最低。这意味着:

  • 避免在循环中使用DOM访问。
  • 将DOM引用分配给局部变量,并使用这些局部变量。
  • 在可能的情况下使用selector API。
  • 当在HTML容器中重复使用时,缓存重复的次数(参考第二章)。

  请看如下范例,尽管第二种方式循环语句更长,但针对不同的浏览器,它会比第一种方法快上几十倍到几百倍。

// 反模式
for (var i = 0; i < 100; i+= 1) {
    document.getElementById('result').innerHTML += i + " ,";
}
// 更好的方式,使用了局部变量
var i, content = " ";
for (let i = 0; i < 100; i+= 1) {
    content += i + " ,";
}
document.getElementById("result").innerHTML  = content

  接下来的一个片段中第二个范例是更好的使用方法(使用了局部变量风格),尽管其需要额外的一横代码和一个变量:

// 反模式
var padding = document.getElementById("result").style.padding,
    margin = document.getElementById("result").style.margin;

// 更好的做法
var style = document.getElementById("result").style,
    padding = style.padding,
    margin = style.margin;

  可以采用如下方法来使用selector API:

document.querySelector("ul .selected");
document.querySelectorAll("#widget .class");

  这些方法接受一个CSS选择字符串并返回一个匹配该选择的DOM节点列表。该选择方法在现在主流的浏览器(IE从8.0以后都支持)中都是支持的,并且会比使用其他DOM方法来自己实现选择要快得多。最近一些最新版本的流行JavaScript库利用了selector API,因此最好是使用个人喜好的最新版本的JavaScript库。

  为经常访问的元素增加id属性是一个很好的做法,因为document.getElementById(myid)是最简单快捷查找节点的方法。

操纵DOM

除了访问DOM元素以外,通常还需要修改、删除或增加DOM元素。更新DOM会导致浏览器重新绘制品目,也经常会导致reflow(也就是重新计算元素的几何位置),这样会带来巨大的开销。

  通常的经验法则是尽量减少更新DOM,这也就意味着将DOM的改变分批处理,并在“活动”文档书之外执行这些更新。

  当需要创建一个相对比较大的子树,应该在子树完全创建之后再将子树添加到DOM树中。这时可以采用文档碎片(document fragment)技术来容纳所有节点。

  下面将介绍如何不立即添加节点:

// 反模式
// 在创建时立即添加节点

var p,t;

p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
document.body.appendChild(p);

p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
document.body.appendChild(p);

  创建文档碎片来离线升级节点信息是更好的做法。当将文档碎片添加到DOM树时,不是将碎片本身添加到DOM树中,而是将文档碎片的内容添加进DOM树中。该操作是十分方便的。文档碎片是一种很好的方法,可以用来封装许多节点信息,甚至这些节点并没有合适的父节点(例如,文章不在div元素范围内)。

  接下来是一个使用文档碎片的范例:

var p,t, frag;

frag = document.createDocumentFragment();
p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
frag.appendChild(p);


p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
frag.appendChild(p);

document.body.appendChild(frag);

  在这个范例中活动的文档仅仅更新了一次并触发一次屏幕重绘。而如果采用之前的反模式,没执行一个段落都会重绘一次。

  在为DOM树添加新节点时文档碎片是非常有用的。但在更新DOM现有的部分时,仍然可以批处理提交修改。具体方法是:为需要修改的子树的根节点建立一个克隆景象,然后对该克隆景象做所有的修改操作操作,在完成修改操作后用克隆镜像替换原来的子树。

var oldnode = document.getElementById('result'),
    clone = oldnode.cloneNode(true);

// 处理克隆镜像...

// 完成后:
oldnode.parentNode.replaceChild(clone, oldnode);

事件

处理浏览器事件(例如单击、鼠标移动等)是浏览器脚本领域中一个有许多不一致性并导致工作失败的源头。JavaScript库可以减少为了支持IE(在IE9.0之前的版本)和符合W3C规范的实现所做的双重工作。

  让我们重温关于浏览器事件的要点,因为可能并不总是为简单的网页使用某个现有的库,有可能还会创建自己的库。

事件处理

通常事件处理是通过为元素附加事件监听器来实现的,例如有一个按钮,该按钮在每次单击后都会增加一次计数。可以增加一个内联的onclick属性,该属性在所有的浏览器中都可以正常工作,但是该属性会和关注分离和渐进增强有冲突。因此,应该争取在JavaScript中附加监听器,并放置于所有标记之外。

  假定有如下标记:

<button id="clickme">Click me: 0</button>

  可以为该节点的onclick属性分配一个函数,但这种做法只能指定一个函数:

// 次优解决方案
var b = document.getElementById('clickme'),
    count = 0;

b.onclick = function () {
    count += 1;
    b.innerHTML = "Click me: " + count;
}

  如果希望在一次单击后执行多个函数功能,仍然维持采用现在的松耦合模式是无法做到的。技术上来说,可以检查onclick是否已经包含一个函数,如果包含了一个函数,那么就将现有的函数功能添加到新函数中,并用新函数替换onclick中的原有函数的属性。但更清晰的方法是使用addEventListener()方法。在IE8.0之前的版本中没有该方法,在这些老版本浏览器中应该使用attachEvent()。

  让我们回顾一下初始化分支模式(参考第四章),可以看到定义跨浏览器事件监听器工具的一种比较好的实现范例。现在无序探究所有的细节,让我们先尝试为按钮添加一个监听器:

var b = document.getElementById('clickme');
if(document.addEventListener) { //W3C
    b.addEventListener('click',myHandler,false);
} else if(document.attachEvent) { // IE
    b.attachEvent('onclick', myHandler);
} else { // 终极手段
    b.onclick = myHandler;
}

  现在一旦按钮被点击,myHandler()函数将会执行,该函数会增加按钮上面“clickme:0”中的数值。让我们假定有多个按钮,并且这些按钮共享同一个myHandler()函数。考虑到可以从每次点击时创建的事件对象中获取数值,因此为每个数值维持按钮节点和计数器之间引用是十分低效的。

  让我们先来看看对此的解决方案,然后再加以评论:

function myHandler(e) {
    var src, parts;

    // 获取事件和源元素
    e = e || window.event;
    src = e.target || e.srcElement;

    // 实际工作:升级标签
    parts = src.innerHTML.split(": ");
    parts[1] = parseInt(parts[1], 10) + 1;

    src.innerHTML = parts[0] + ": " + parts[1];

    // 无冒泡
    if(typeof e.stopPropagation === 'function') {
        e.stopPropagation();
    }
    if(typeof e.cancelBubble !== 'undefined') {
        e.cancelBubble = true;
    }

    // 阻止默认操作
    if (typeof e.preventDefault === "function") {
        e.preventDefault();
    }
    if (typeof e.returnValue !== "undefined") {
        e.returnValue = false;
    }
}

  这个事件处理函数分为四个部分:

  • 首先需要获取对事件对象的访问权,该事件对象包含了关于事件和触发该事件的网页元素的信息。事件对象被传递给回调事件处理器,而不是使用o'clock属性(可以通过全局属性windows.event来获取访问权)。
  • 第二部分是处理升级标签的实际工作。
  • 接下来第三部分是取消事件的传播。在当前特定的范例中,这一部分可以省略,不是必须的。但是通常如果不这样做,会导致事件传播到根文档,甚至是传播到window对象中。在这个部分需要采用两种方法实现,一种是W3C标准方法(stopPropagation());另外一种是IE特有的方法(cancelBubble)。
  • 最后,如果需要时,要阻止执行默认操作。一些事件拥有默认操作,但可以使用preventDefault()来阻止默认操作(在IE中,通过将returnValue设置为false来实现)。

  如您所见,这样的做法包含很多重复性工作,因此按照第7章讨论的那样使用正面方法创建自己的事件工具是十分有意义的。

  上面代码的示例地址在http://www.jspatterns.com/book/8/click.html

事件授权

事件授权模式得益于事件冒泡,会减少为每个节点附加的事件监听器数量。如果在div元素汇总有10个按钮,只需要为该div元素附加一个事件监听器就可以实现为每个按钮分别附加一个监听器的效果。

  我们可以简单的来看一个示例:

<div id="click-wrap">
  <button>Click me: 0</button>
  <button>Click me too: 0</button>
  <button>Click me three: 0</button>
</div>

  可以使用如上的标记,可以通过为“click-wrap”div附加监听器来代替为每一个按钮都附加监听器。然后只需要对之前范例中使用的myHandler()函数做微小修改(需要过滤不感兴趣的点击事件),就可以直接使用。在这种情况下,只需寻找按钮的点击事件,而同一个div元素中其他点击事件都会被忽略。

  对myHandler()需要做的修改就是判断时间的nodeName是否为“button”,如果是,则执行函数功能:

// ...
// 获取事件和源元素
e = e || window.event;
src = e.target || e.srcElement;
if(src.nodeName.toLowerCase() !== "button") {
    return;
}
// ...

  事件授权的缺点在于如果碰巧没有感兴趣的事件发生,那么增加的小部分代码就显得没用了。但是采用该模式所获的收益(性能和更为清晰的代码)远远大于缺点,因此强烈推荐使用该模式。

  最近的JavaScript库通过API,使得事件授权更为简便。举例来说,YUI3有一个Y.delegate()方法,该方法可以制定一个CSS选择器来匹配封装,并使用另外一个选择器来匹配感兴趣的节点。这是十分方便的,因为当事件在关注的节点之外发生时,回调事件函数实际上并没有被调用。在这种情形下,附加一个事件监听器的代码是十分简便的,如下所示:

Y.delegate('click', myHandler, "#click-wrap", "button");

  由于YUI将各种浏览器的区别抽象出来了,可以由用户决定事件的来源,因此回调函数将变得更为简便:

function myHandler(e) {
    
    var src = e.target,
        parts;
    
    parts = src.get('innerHTML').split(": ");
    parts[1] = parseInt(parts[1], 10) + 1;
    src.set('innerHTML', parts[0] + ": " + parts[1]);
    
    e.halt(); 
}

  完整的例子在http://www.jspatterns.com/book/8/click-y-delegate.html