在本书的前面章节中,我们主要集中关注于核心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

《JavaScript 模式》读书笔记(8)— DOM和浏览器模式1的更多相关文章

  1. 《编写可维护的javascript》读书笔记(中)——编程实践

    上篇读书笔记系列之:<编写可维护的javascript>读书笔记(上) 上篇说的是编程风格,记录的都是最重要的点,不讲废话,写的比较简洁,而本篇将加入一些实例,因为那样比较容易说明问题. ...

  2. 《你不知道的javascript》读书笔记2

    概述 放假读完了<你不知道的javascript>上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用. 这篇笔记是这本书的下半部分,上半部分请见<你不知道的java ...

  3. 《Javascript模式》之对象创建模式读书笔记

    引言: 在javascript中创建对象是很容易的,可以使用对象字面量或者构造函数或者object.creat.在接下来的介绍中,我们将越过这些方法去寻求一些其他的对象创建模式. 我们知道js是一种简 ...

  4. Javascript & JQuery读书笔记

    Hi All, 分享一下我学JS & JQuery的读书笔记: JS的3个不足:复杂的文档对象模型(DOM),不一致的浏览器的实现和便捷的开发,调试工具的缺乏. Jquery的选择器 a. 基 ...

  5. JavaScript设计模式 -- 读书笔记

    JavaScript设计模式 一. 设计模式 一个模式就是一个可重用的方案: 有效的解决方法.易重用.善于表达该解决方案: 未通过"模式特性"测试的模式称为模式原型: 三规则:适用 ...

  6. 《面向对象的JavaScript》读书笔记

    发现了2004年出版的一本好书,用两天快速刷了一遍,草草整理了一下笔记,在此备忘. 类:对象的设计蓝图或制作配方. 对象 === 实例 :老鹰是鸟类的一个实例 基于相同的类创建出许多不同的对象,类更多 ...

  7. JavaScript设计模式:读书笔记(未完)

    该篇随我读书的进度持续更新阅读书目:<JavaScript设计模式> 2016/3/30 2016/3/31 2016/4/8 2016/3/30: 模式是一种可复用的解决方案,可用于解决 ...

  8. 《Head First 设计模式》读书笔记(1) - 策略模式

    <Head First 设计模式>(点击查看详情) 1.写在前面的话 之前在列书单的时候,看网友对于设计模式的推荐里说,设计模式的书类别都大同小异,于是自己就选择了Head First系列 ...

  9. SQL反模式读书笔记思维导图

    在写SQL过程以及设计数据表的过程中,我们经常会走一些弯路,会做一些错误的设计.<SQL反模式>这本书针对这些经常容易出错的设计模式进行分析,解释了错误的理由.允许错误的场景,并给出更好的 ...

  10. Java与模式读书笔记

    >设计目标:可扩展性,灵活性,可插入性. >设计原则 ● Open Closed Principle 开闭原则 对扩展开放,对修改关闭. 对面向对象的语言来说,不可以更改的是系统的抽象层, ...

随机推荐

  1. OData – How It Work

    前言 OData 是很冷门的东西, 用的人少, 开发的人少, 文档自然也少的可怜. 如果真的想用它, 多少要对它机制有点了解. 这样遇到 bug, 想扩展的时候才不至于完全没有路. 主要参考: ODa ...

  2. Figma 学习笔记 – Plugin

    安装 Figma 安装 plugin 基本上就是点击一下开启而已. 到社区搜索 -> 点击 install Material Icon 下载地址 它的交互不是 drag 出来哦, 而是点击 ic ...

  3. httpclient调用接口

    有时候会将参数(返回结果)压缩(解压),加密(解密) 将json参数通过GZip压缩 Base64加密 1 public static String gzipAndEncryption(String ...

  4. [namespace hdk] diff.h

    Example cth.txt 12345 54321 114514 hdk.txt 12345 54321 114514 #include"diff.h" using names ...

  5. linux那些事之页迁移(page migratiom)

    Page migration 页迁移技术是内核中内存管理的一种比较重要的技术,最早该技术诞生于NUMA系统中(Page migration [LWN.net]),后续由于内存规整以及CMA和COW技术 ...

  6. 001 (Python+水论文合集)为什么录这个合集,这个合集会讲哪些内容?

    博客配套视频链接: https://space.bilibili.com/383551518?spm_id_from=333.1007.0.0 b 站直接看 配套 github 链接:https:// ...

  7. Monaco Editor 实现一个日志查看器

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:文长 前言 在 Web IDE 中,控制台中展示日志是至关 ...

  8. CNI 基准测试:Cilium 网络性能分析

    原文链接:https://cilium.io/blog/2021/05/11/cni-benchmark 作者:Thomas Graf 译者:罗煜.张亮,均来自KubeSphere 团队 Thomas ...

  9. 最后的组合:K8s 1.24 基于 Hekiti 实现 GlusterFS 动态存储管理实践

    前言 知识点 定级:入门级 GlusterFS 和 Heketi 简介 GlusterFS 安装部署 Heketi 安装部署 Kubernetes 命令行对接 GlusterFS 实战服务器配置(架构 ...

  10. NeuVector 会是下一个爆款云原生安全神器吗?

    近日一则<SUSE 发布 NeuVector:业内首个开源容器安全平台>的文章被转载于各大 IT 新闻网站.作为 SUSE 家族的新进成员,在 3 个月后便履行了开源承诺,着实让人赞叹.那 ...