浏览器对HTML的容错处理

说明: webkit开源的主干trunk中,这部分代码已经于2012-04-24更新,现在的版本来源于google公司,很多逻辑和规则发生了变化,文中代码在ie6/7/8、chrome、firefox、safari、opera的最新版本中测试(基于html5的DTD<!DOCTYPE html>),本文内容仅供处理各种bugs时参考,代码书写请遵循最新的HTML规范正确嵌套和闭合标签

引言:前段时间看浏览器工作原理(How Browsers Work: Behind the scenes of modern web browsers),其中提到浏览器(webkit)对html标签的处理:

Unfortunately, we have to handle many HTML documents that are not well-formed, so the parser has to be tolerant about errors.

We have to take care of at least the following error conditions:

The element being added is explicitly forbidden inside some outer tag. In this case we should close all tags up to the one, which forbids the element, and add it afterwards.

We are not allowed to add the element directly. It could be that the person writing the document forgot some tag in between (or that the tag in between is optional). This could be the case with the following tags: HTML HEAD BODY TBODY TR TD LI (did I forget any?).

We want to add a block element inside to an inline element. Close all inline elements up to the next higher block element.

If this doesn’t help, close elements until we are allowed to add the element or ignore the tag.

按照自己的理解粗略翻译了一下这四条:

元素被放置在明确禁止的标签容器内,元素会被追加到标签容器之后。

不能直接添加某些元素,有些元素必须包含子元素,有些元素必须在特定的标签内。

内联元素里面添加块元素,实际是可行的。

如果这些都不能解决问题,就先闭合这个元素,然后在浏览器引擎认为可以添加这个元素的地方追加(参考第1、3条)、或者忽略这个标签。

浏览器的容错表现

因为webkit的代码更新,上面的一些规则可能已经发生了变化,以下是现在大部分浏览器,对一些错误嵌套和自闭和的处理表现。

不能单独使用的标签

  • 需要子元素 html/head/body/table/ul….
  • 需要父元素 tbady/tr/td/li/dd …

复杂的错误嵌套处理

  • 嵌套错误时,块元素里面包含的元素都会先自闭和
  • 嵌套错误时,内联元素里面包含的是块元素,则内联元素本身会自闭和(非ie)
    <strong>strong<div>div</strong>p+</div> 处理成 <strong>strong</strong><div><strong>div</strong>p+</div>
    <strong>strong<em>em</strong>em+</em> 处理成 <strong>strong<em>em</em></strong><em>em+</em>
    <div>ddd<h2>h2</div>h2+</h2> 处理成 <div>ddd<h2>h2</h2></div>h2+
    <div>ddd<em>em</div>em+</em> 处理成 <div>ddd<em>em</em></div><em>em+</em>

自闭和标签

  • <br><br /></br> 在标准浏览器下都解析成<br>,ie6/7/8下解析成<br />
  • <img>/<img />、<hr>/<hr />同上

独特的P

  • <p>后面跟块元素,或</p>单独出现的时候都可以自闭和
  • 嵌套的p都会被解析成并行的p

被忽略的</body>和</html>

  • 放错位置的</body>和</html>标签,浏览器会特殊处理,忽略这2个标签,编辑器生成的代码中的</body>和</html>不会造成重大灾害

不能嵌套的Form

<form method="post" action="a">
	<input type="text" id="aa" />
	<form method="post" action="b">
		<input type="text" id="bb" />
	</form>
</form>

解析后成了

<form method="post" action="a">
	<input type="text" id="aa">
	<input type="text" id="bb">
</form>

错误的table嵌套

<table>
	<tr><table>
		<tr>
			<td>11</td>
			<td>12</td>
		</tr>
	</table>
		<td>21</td>
		<td>22</td>
	</tr>
</table>

嵌套在非th和td中的table,会生成一个平行的table,解析后成了

<table>
	<tbody>
		<tr></tr>
	</tbody>
</table>
<table>
	<tbody>
		<tr>
			<td>11</td>
			<td>12</td>
		</tr>
	</tbody>
</table>

最多嵌套20层?

  • 这是webkit某一个版本中的限制,浏览器实现的时候并没有沿用这个限制,而且最新的webkit版本中也没有了这个限制
  • 同一个标签嵌套100层是没有问题的 (具体层数各个浏览器不同),但是各个浏览器还是不能无限的嵌套的,过多的嵌套会影响页面渲染性能

关注压缩后的文件大小 针对Gzip压缩的代码优化

Gzip相关

提起Gzip压缩,我们一般关注都是服务器端优化设置,很少关注我们的代码的书写方式对Gzip压缩的影响,其实前端的代码对Gzip压缩的效果也是有影响的。

Gzip的基本压缩算法,简单的理解,就是找出文件中相同的字符串,第一个以后重复的字符串都通过索引指向第一个实体,同时存储文件中所有第一次出现的字符串,和指向他的索引数据,从而到达文件的完整和缩小文件大小的目的(这样处理之后也可能增大文件体积,尤其是很小的文件的时候,google提供的最小文件大小是150 bytes)。

关注压缩后的文件大小

了解了压缩原理之后,就容易理解为什么有些文件增大之后,压缩输出时反而变的更小了。现在性能优化只是普及,大部分的服务器都对文本文件进行了Gzip压缩,因此我们应该关注的是Gzip压缩之后的文件大小,而不是未压缩的文件大小。

下面是google推荐的一些方法,可以在Gzip压缩文件的时候重复利用更多的字符串,用来实现更大的文件压缩率,前端、重构们可以酌情使用。

  • 统一CSS的属性顺序,同一文件中保持统一的属性顺序,比如按字母排序或者自己的固定规则(backgroud后面写color,或者color后面写backgroud,所有的选择器保存统一规则)
  • HTML标签的属性顺序保持一致,规则同上一条,把同一文件中某个标签最常用的属性排在前面,比如a标签的href属性放在第一位,div标签中把class属性放在第一位,其余的按固定规则排(据说google搜索结果中,HTML属性按照字母排序后,Gzip输出的文件减小了1.5%)
  • 保持大小写一致,比如统一小写,复杂的情况下,至少同样的标签和属性再次出现的时候保持大小写一致
  • 统一引号,HTML标签的属性值统一使用双引号(统一使用单引号、或不适用引号,不推荐)。

实例效果

  • HTML标签属性顺序调整:把属性值固定的属性放到最前面,其他按字母排序。
    优化前:list-o.html <a href=”xxxx” target=”_blank”>
    (923 bytes, gzip compressed to 535 bytes ( 42 % saving ))
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="gbk" />
<title>拍拍网:腾讯旗下购物网站</title>
<link rel="stylesheet" href="list-o.css" type="text/css" media="screen" />
</head>
<body>
<div class="icnts">
	<h3 class="tit"><a href="http://paipai.lady.qq.com/a/20120313/000017.htm?PTAG=30595.2.3" target="_blank">宋慧乔VS林依晨 矮个女星穿衣经</a></h3>
	<ul class="list">
		<li><span class="tag">[青春]</span><a href="http://paipai.lady.qq.com/a/20120321/000018.htm?PTAG=30595.2.31" target="_blank">90后俏妞一周穿搭!男友最喜欢</a></li>
		<li><span class="tag">[甜美]</span><a href="http://paipai.lady.qq.com/a/20120321/000017.htm?PTAG=30595.2.32" target="_blank">针织开衫+碎花裙 小肚腩不见了!</a></li>
		<li><span class="tag">[得体]</span><a href="http://paipai.lady.qq.com/a/20120320/000030.htm?PTAG=30595.2.33" target="_blank">气质女孩穿衣15招 长辈男友都爱</a></li>
	</ul>
</div>
</body>
</html>
  • 优化后:list-l.html <a target=”_blank” href=”xxxx”>
    (923 bytes, gzip compressed to 534 bytes ( 42.1 % saving ))
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="gbk" />
<title>拍拍网:腾讯旗下购物网站</title>
<link rel="stylesheet" href="list-l.css" type="text/css" media="screen" />
</head>
<body>
<div class="icnts">
	<h3 class="tit"><a target="_blank" href="http://paipai.lady.qq.com/a/20120313/000017.htm?PTAG=30595.2.3">宋慧乔VS林依晨 矮个女星穿衣经</a></h3>
	<ul class="list">
		<li><span class="tag">[青春]</span><a target="_blank" href="http://paipai.lady.qq.com/a/20120321/000018.htm?PTAG=30595.2.31">90后俏妞一周穿搭!男友最喜欢</a></li>
		<li><span class="tag">[甜美]</span><a target="_blank" href="http://paipai.lady.qq.com/a/20120321/000017.htm?PTAG=30595.2.32">针织开衫+碎花裙 小肚腩不见了!</a></li>
		<li><span class="tag">[得体]</span><a target="_blank" href="http://paipai.lady.qq.com/a/20120320/000030.htm?PTAG=30595.2.33">气质女孩穿衣15招 长辈男友都爱</a></li>
	</ul>
</div>
</body>
</html>
  • CSS属性顺序调整:把属性值固定的属性按字母排序放到最前面(或最后),其他按字母排序。
    优化前:list-o.css (1055 bytes, gzip compressed to 522 bytes ( 50.5 % saving ))
body,h1,h2,h3,h4,h5,h6,p,blockquote,dl,dt,dd,ul,ol,li,pre,form,fieldset,legend,button,input,textarea,th,td,figure{margin:0;padding:0}
body,button,input,select,textarea{font:12px/1.5 arial;}
h1,h2,h3,h4,h5,h6,button,input,select,textarea{font-size:100%;}
address,cite,dfn,em,var{font-style:normal;}
code,kbd,pre,samp{font-family:courier new,courier,monospace;}
small{font-size:12px;}
ul,ol{list-style:none;}
img{border:0;}
a{text-decoration:none;color:#369;}
a:hover{text-decoration:underline;color:#c00;}

.icnts .tit{height:39px;padding-top:2px;line-height:36px;overflow:hidden;font:bold 18px "Microsoft Yahei";text-overflow:ellipsis;white-space:nowrap;width:328px;}
.icnts .tit a{color:#454545;}
.icnts .tit a:hover{color:#ff4e00;}
.icnts .list{padding-bottom:7px;margin-bottom:14px;}
.icnts .last{border-bottom:0;margin-bottom:0;padding-bottom:0;}
.icnts .list li{padding-bottom:3px;height:23px;font-size:14px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;-width:100%;}
.icnts .list .tag{color:#979797;padding-right:8px;}
  • 优化后:list-l.css (1055 bytes, gzip compressed to 514 bytes ( 51.3 % saving ))
    …text-overflow:ellipsis;white-space:nowrap;overflow:hidden;}
body,h1,h2,h3,h4,h5,h6,p,blockquote,dl,dt,dd,ul,ol,li,pre,form,fieldset,legend,button,input,textarea,th,td,figure{margin:0;padding:0}
body,button,input,select,textarea{font:12px/1.5 arial;}
h1,h2,h3,h4,h5,h6,button,input,select,textarea{font-size:100%;}
address,cite,dfn,em,var{font-style:normal;}
code,kbd,pre,samp{font-family:courier new,courier,monospace;}
small{font-size:12px;}
ul,ol{list-style:none;}
img{border:0;}
a{color:#369;text-decoration:none;}
a:hover{color:#c00;text-decoration:underline;}

.icnts .tit{font:bold 18px "Microsoft Yahei";height:39px;line-height:36px;padding-top:2px;width:328px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;}
.icnts .tit a{color:#454545;}
.icnts .tit a:hover{color:#ff4e00;}
.icnts .list{margin-bottom:14px;padding-bottom:7px;}
.icnts .last{border-bottom:0;margin-bottom:0;padding-bottom:0;}
.icnts .list li{font-size:14px;height:23px;padding-bottom:3px;-width:100%;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;}
.icnts .list .tag{color:#979797;padding-right:8px;}
  • HTML标签统一小写、属性值用双引号,这些都是标准化的标配,此处不再分析。

说明:文件部署于内部服务器,分析数据通过HttpWatch抓包收集。

小结:

  1. 使代码中重复出现的字符串尽可能多,匹配的字符串长度尽可能长;
  2. 实例中的代码重复量较小,效果不是特别明显,但可以看出优化是有效的,优化效果因文件内容的差异而不同。

参考:

Microdata – HTML5之语义扩展

语义网是一个梦想:互联网上所有的内容都是数据,这些数据和现实世界的具体事物相关,各个应用程序和设备都可以识别和使用这些数据。HTML标签本身在不断朝着语义化方向发展,但是这些标签数量有限,远远不能满足需求,于是一些其他的技术出来,用来增强和扩展语义。

大部分网页都是以HTML格式被呈现的,而前端攻城师每天接触的都是HTML文件,于是我们编写的代码直接影响着语义网的发展…^^*…这个话说的有点大了,不过我们在写HTML标签的时候,多考略一点语义化的东西还是有不少作用的。

  1. 清晰明了的代码,便于代码的阅读、维护、复用;
  2. 更好的可访问性,被更多的设备访问;
  3. 对搜索引擎友好,内容被搜索引擎更好的理解、分析和索引。

前端攻城师们在他们的代码中使用各种技术来增强网页的语义。

  1. 使用语义化的标签和结构,主标题用<h1>,头部用<header>(HTML5)等。
  2. 书写语义化的属性,容易理解的class名,比如class=”article-name”、procuct-tag=”xxx yyy zzz”等

但是,HTML的标签总是有限的,虽然HTML5草案中新增了不少的标签,但是远远不能满足所有开发人员的需求,而且这些标签只是描述了一些网页上常见的内容,还有更多的内容没有合适的标签,比如一个人以及相关的名字、地址、性别、联系方式等;而自定义的一些属性,比如procuct-tag=”xxx yyy zzz”,只能被本站的代码、或者仅仅是这个功能的开发人员所能识别和理解,其他设备和搜索引擎根本无法识别。

语义扩展之道 Microformats / RDFa / Microdata

于是微格式出现了,RDFa出现了,这些技术很简单,能够增前并扩展页面的语义,对,是扩展语义,让页面的文字不再是文字,变成了一种数据,可以被程序或者机器、搜索引擎等理解的数据。

最早的Microformats,通过特定的class名来实现,应用简单,主流搜索引擎支持:

<div class="vcard">
   My name is
   <span class="fn">Bob Smith</span>,
   Here is my home page:
   <a href="http://www.example.com" class="url">www.example.com</a>.
</div>

之后的RDFa,通过自定义属性-值的方式来实现,在扩展性方面做的很好,需要声明用来解析的词汇库,使用起来复杂一点,最近退出的RDFa lite有更进一步的改善,兼容shcema.org的词汇库,主流搜索引擎支持:

<div xmlns:v="http://rdf.data-vocabulary.org/#" typeof="v:Person">
   My name is <span property="v:name">Bob Smith</span>,
   Here is my homepage:
   <a href="http://www.example.com" rel="v:url">www.example.com</a>.
</div>

Microdata和RDFa一脉相承,只是使用语法更加简单明了,出现了被三大搜索引擎支持的词汇库shcema.org,主流搜索引擎支持:

<div itemscope itemtype="http://data-vocabulary.org/Person">
  My name is <span itemprop="name">Bob Smith</span>,
  Here is my homepage:
  <a href="http://www.example.com" itemprop="url">www.example.com</a>.
</div>

如何选择?

这三种方案是兼容的并不排斥,你可以在同一个页面使用不同的方案,并且可以在一个页面上使用多种方案,但这会给各种设备和搜索引擎增减兼容逻辑,而且三个独立发展,力量分散,前进的步子跨的不是很快,那我们应该使用哪一种呢:

  1. 技术可行性,简单易用、兼容性?
  2. 什么样的收益?
  3. 发展方向、标准化?

我们根据最新的一些消息来分析一下

  1. Microdata结合了Microformats的简单易用和RDFa的可扩展性;
  2. Microdata出现在HTML5草案中;
  3. 三大搜索引擎联合推出用于Microdata的词汇库shcema.org,在搜索结果页显示相关的富网摘。

Microdata+shcema 胜出

Microdata基本用法(whatwg版本)

<div itemscope itemtype="http://schema.org/Person">
  My name is <span itemprop="name">Bob Smith</span>
  Here is my homepage:
  <a href="http://www.example.com" itemprop="url">www.example.com</a>.
</div>
  • 通过itemscope来声明一个数据对象;
  • 用itemtype=”http://schema.org/Xxx”来指定使用的词汇库,以及数据的类型
  • 通过标签的itemprop=”yyyy”来指定对象的特定属性,标签包含的内容就是属性的值(有少数例外,可参考http://www.schema.org/docs/full.html);
  • DOM API     document.getItems(“http://schema.org/Person”)    opera11.60+

schema.org中定义的数据类型列表比较多,文档以及对应词汇表为 http://schema.org/

富网摘(Rich Snippets)

其实,现阶段,语义扩展带来的唯一的看的见的好处是:页面内容更好的被搜索引擎理解,以及看得见的搜索引擎的富网摘(Rich Snippets)。而这个是Microformats、RDFa和Microdata都可以实现的,下面以Microdata为例,简单说明一下如何在项目中使用。

1.添加属性

<div itemscope="itemscope" itemtype="http://schema.org/Product">
	<h1 itemprop="name">三星I9100(Galaxy S 2)2.3系统双核 超值热卖</h1>
	<a href="http://auction1.paipai.com/F4E679240000000000743B3E0800C6B9"><img itemprop="image" src="http://img4.paipaiimg.com/b092a3be/item-0E798018-F4E679240000000000743B3E0800C6B9.0.300x300.jpg" alt="三星I9100(Galaxy S 2)2.3系统双核 超值热卖" /></a>
	<div itemprop="aggregateRating" itemscope="itemscope" itemtype="http://schema.org/AggregateRating">
		<span itemprop="ratingValue">3.5</span>/5
		<span itemprop="reviewCount">766</span> 人评价
	</div>
	<div itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer">
		<span itemprop="price">3030.00</span>
		<meta itemprop="priceCurrency" content="CNY" />
		<link itemprop="availability" href="http://schema.org/InStock" />现货 可购买
	</div>
</div>

2.测试google对数据的提取,以及预览效果 Rich Snippets Testing Tool

3.正式应用到模版

tips:

  1. 面包屑、评论和价格预览是富网摘中可以看到成果的东西
  2. 可以用搜索引擎对Microdata识别的兼容性测试工具(google),来预览页面在搜索引擎结果页的效果,同时检测Microdata用法是否准确。http://www.google.com/webmasters/tools/richsnippets
  3. 不同的词汇表(主要是schema.org与data-vocabulary.org),对应的属性和值稍有不同,有些词汇的某些属性是必须有的,需对照文档使用,搜索引擎结果页并不会显示所有设置了的词汇类型和属性。
  4. 设置的词汇类型和属性越多越好,不是所有的属性都会显示为富网摘。
  5. 属性名或者属性值不对,嵌套关系不对会导致,设置了属性之后通过样式隐藏的元素等都会导致富网摘不会出现。

slideshare.net: microdata-semanticextend

高性能Javascript【八】编程实战

  • 避免因使用eval()和Function()构造器带来的双重求值造成的性能消耗,同样,设置setTimeout()和setInterval()的参数为函数,而不是字符串。
  • 尽量使用直接量创建对象和数组,直接量的创建和初始化都比非直接量形式要快。
  • 避免做重复工作,可以使用延迟加载或条件预加载来检测浏览器。
  • 进行数学计算时,可以考虑用位运算,直接操作数字的二进制形式。
  • 尽量使用原生方法,javascript的原生方法总比你鞋的任何代码都要快。

避免双重求值(Double Evaluation)

javascript允许提取一个包含代码的字符串,然后动态执行,有四种标准的方法可以实现:eval()、Function()构造函数、setTimeout()、setInterval()。

var num1 = 5,num2 = 6,
//eval() 执行代码字符串
result = eval("num1 + num2"),
//Function() 执行代码字符串
sum = new Function("arg1", "arg2", "return arg1 + arg2");
//setTimeout() 执行代码字符串
setTimeout("sum = num1 + num2", 100);
//setInterval() 执行代码字符串
setInterval("sum = num1 + num2", 100);

在代码中动态执行另一段代码,会导致双重求值的性能消耗:代码先以正常的方式求值,在执行过程中,包含于字符串中的代码会发起另一次求值运算,双重求值比正常的代码执行速度要慢许多,因为每一双重求值都会创建一个新的解释器/编译器实例,因此需要避免使用eval()和Function(),而setTimeout()和setInterval()则用函数代替字符串作为参数。

//正常
var item = array[0];
//双重求值
var item = eval("array[0]");
//用函数作为参数
setTimeout(function(){
sum = num1 + num2;
}, 100);
setTimeout(funcName, 100);

使用对象和数组直接量

对比new和直接量,直接量不但运行更快,而且更加节省代码,尤其在属性成员很多的时候优势更明显。

//直接量创建和初始化对象
var myObject = {
  name: "Nicholas",
  count: 50,
  flag: true,
  pointer: null
};
//new创建和初始化对象
var myObject = new Object();
myObject.name = "Nicholas";
myObject.count = 50;
myObject.flag = true;
myObject.pointer = null;
//直接量创建和初始化数组
var myArray = ["Nicholas", 50, true, null];
//new创建和初始化数组
var myArray = new Array();
myArray[0] = "Nicholas";
myArray[1] = 50;
myArray[2] = true;
myArray[3] = null;

不要重复工作

不要重复工作包含2个方面:不要做不必要的工作,不要重复已经做过的工作。

function addHandler(target, eventType, handler){
  if (target.addEventListener){ //DOM2 Events
    target.addEventListener(eventType, handler, false);
  } else { //IE
    target.attachEvent("on" + eventType, handler);
  }
}

上面是我们常见的添加事件的函数,每次只要使用addHandler就可以了,避免了重复的代码,但是每次调用函数时都做了重复的工作:每次检测特定的方法是否存在,即判断浏览器类型。但是程序运行之后,她运行的环境是不会再变的,可以减少这个重复的工作。

延迟加载

延迟加载意味着在你没有使用的时候,他不会做任何操作。

还是上面的例子,用延迟加载处理之后,函数调用前不会进行任何的浏览器判断逻辑。只有在第一次调用时会先判断浏览器,然后整个函数根据浏览器特性重写,再运行新的函数;之后每次调用,都不会再判断浏览器,因为新的函数中这个判断的逻辑已经没有了,但是函数第一次运行的时候会耗时较长,但是后续再调用的时候会很快,因为不需要再重复第一次的检测逻辑。当函数不会被立刻调用的时候,延迟加载是最好的选择。

function addHandler(target, eventType, handler){
  //根据浏览器特性重新函数
  if (target.addEventListener){ //DOM2 Events
    //新的函数
	addHandler = function(target, eventType, handler){
      target.addEventListener(eventType, handler, false);
    };
  } else { //IE
    //新的函数
	addHandler = function(target, eventType, handler){
        target.attachEvent("on" + eventType, handler);
    };
  }
  //调用新的函数
  addHandler(target, eventType, handler);
}

条件预加载

和延迟加载一样,只会进行一次判断,只是判断逻辑提前了,条件预加载会在脚本加载时提前判断浏览器,然后把对应的操作保存在变量中,被调用的时候不再进行任何判断。试用与马上会被使用,并且会重复被使用的情况。

var addHandler = document.body.addEventListener ?
  function(target, eventType, handler){
    target.addEventListener(eventType, handler, false);
  }:
  function(target, eventType, handler){
    target.attachEvent("on" + eventType, handler);
  };

使用速度快的部分

位操作 Bitwise_Operators

javascript中的数字都是依照IEEEE-754标准,以64位格式存储,在位操作中,数字被转化为有符号的32位二进制,每次运算符会直接操作该32位数以得到结果,这个过程比javascript其他数学运算和布尔操作要快很多,而且位运算返回的结果是正常的十进制数字。

javascript中的toString()方法,可以很容易的把数字转化为二进制表达式(结果忽略了数字高位的0):

var num1 = 25,num2 = 3;
alert(num1.toString(2)); // "11001"
alert(num2.toString(2)); // "11"

位与操作 bitwise AND

两个操作数的对应位都是1时,则在该位返回1。

//bitwise AND
var result1 = 25 & 3; //1
alert(result.toString(2)); //"1"

位或操作 bitwise OR

两个操作数的对应位只要有一个为1时,则在该位返回1。

//bitwise OR
var result2 = 25 | 3; //27
alert(resul2.toString(2)); //"11011"

位异或操作 bitwise XOR

两个操作数的对应位只有一个为1时,则在该位返回1。

//bitwise XOR
var result3 = 25 ^ 3; //26
alert(resul3.toString(2)); //"11000"

位反操作 bitwise NOT

遇到0则返回1,反之亦然。任何数值 n 的位反等于 -(n + 1),即~n === -(n + 1)

//bitwise NOT
var result = ~25; //-26
alert(resul2.toString(2)); //"-11010"

使用位操作代替纯数学操作,比如通常采用对2取模运算实现表格行颜色交替:

for (var i=0, len=rows.length; i < len; i++){
  if (i % 2) {
    className = "even";
  } else {
    className = "odd";
  }
  //apply class
}

在32位二进制中,偶数的最低位是0,奇数的最低位是1,这样就可以通过对给定数字和1进行位与操作,偶数和1的位与运算结果为0,奇数和1的位与运算结果为1,于是上面的代码可以优化为(快50%,取决于浏览器):

for (var i=0, len=rows.length; i < len; i++){
  if (i & 1) {
    className = "odd";
  } else {
    className = "even";
  }
  //apply class
}

“位掩码”,用来处理同时存在多个布尔选项的情形

var OPTION_A = 1;
var OPTION_B = 2;
var OPTION_C = 4;
var OPTION_D = 8;
var OPTION_E = 16;
var options = OPTION_A | OPTION_C | OPTION_D;
//is option A in the list?
if (options & OPTION_A){
  //do something
}
//is option B in the list?
if (options & OPTION_B){
  //do something
}

具体实现原理:

1 -> 0 0 0 0 0 0 0 1;
2 -> 0 0 0 0 0 0 1 0;
4 -> 0 0 0 0 0 1 0 0;
8 -> 0 0 0 0 1 0 0 0;
16-> 0 0 0 1 0 0 0 0;

当它们取 | 时,因为各个位上都是 1, 所以如果项为选中状态时为: 0 0 0 1 1 1 1 1;即每个位代表一个选项。在判断是否为选中状态时,只需要使用各自原始值与最后状态值取&即可,结果为0,代表当前状态是未选中,否则为选中状态。

利用位反操作来简化indexOf判断

因为任何数值 n 的位反等于 -(n + 1),只有 ~-1 才等于 0. 对非 -1 值取反永远不为 0;而indexOf灭有找到是返回的正好也是-1,于是

if (str.indexOf('sub') !== -1) {
// code
}

利用位反操作,可简化为:

if (~str.indexOf('sub')) {
// code
}

更普适的规律是:

n !== -1 可简化为 ~n
n === -1 可简化为 !~n

原生方法

因为原生方法存在与浏览器中,并且是低级语言编写的(比如C++),这意味着这些方法已经是机器码,成为了浏览器的一部分,用编译后的代码是最快的。这方面比较典型的是用于数学运算MATH和DOM中的选择器API。

常见的数学常量和方法:

Math.E E的值,自然对数的底
Math.LN10 10的自然对数
Math.LN2 2的自然对数
Math.LOG2E 以2为底的E的对数
Math.LOG10E 以10为底的E的对数
Math.PI 圆周率π的值
Math.SQRT1_2 1/2的平方根
Math.SQRT2 2的平方根
Math.abs(num) num的绝对值
Math.exp(num) E的指数
Math.log(num) num的自然对数
Math.pow(num,power) num的power次幂
Math.sqrt(num) num的平方根
Math.acos(x) x的反余弦值
Math.asin(x) x的反正弦值
Math.atan(x) x的反正切值
Math.atan2(y,x) X轴到(y,x)点的角度
Math.cos(x) x的余弦值
Math.sin(x) x的正弦值
Math.tan(x) x的正切值

使用原生的querySelector() 和 querySelectorAll()消耗只有使用自己编写的方法的10%。测试数据来源:http://www.webkit.org/perf/slickspeed/

tips:

优化后的javascript引擎通常会缓存使用了eval()且重复运行的代码,比如safari 4+和chrome。

javascript也支持按位左移(<<),按位右移(>>),和无符号右移等位运算符。

Chorme实现了很多的javascript原生方法,chrome对原生方法和用户自己编写的代码都使用即时javascript编译器,所以他们之间的性能差异不大。

高性能Javascript【七】Ajax

这一章的作者是前面写了字符串和正则表达式部分的steven levithan,高性能Ajax需要根据项目的具体情况和要求,选择合适的数据格式和数据传送技术。

  • 纯文本和HTML只适用于特定的场合,但是可以客户端的CPU消耗;
  • XML被广泛应用和支持,但是比较笨重且解析缓慢;
  • JSON是轻量级的,解析速度快(是js原生对象而不是字符串),通用性与XML相当;
  • 字符分隔的自定义格式更加轻量级,解析大量数据时非常快,但是需要在服务器和客户端都进行特殊处理。
  • XHR在同域名下接收数据时具有很好的灵活性和可控性,不过解析速度会可能会收到影响,因为接收到的数据是需要处理的字符串;
  • 动态脚本注入可以跨域接收数据,并且数据是不需要解析的原生javascript或json代码,但是可控性不强,不能读取头信息和响应代码;
  • Multipart XHR接收数据时可以用来减少请求数,她能在同一个响应中处理多个文件和多种文件类型,但是不能缓存接收到的数据。
  • 图片信标是一种简单而有效的发送数据方法,当然XHR也可以用POST方法发送大量数据。

加速Ajax

  • 减少请求,通过合并javascript和css文件,或者使用Multipart XHR;
  • 缩短页面加载时间,主要内容先加载,之后用ajax来获取次要的内容;
  • 在服务器端做好容错,确保代码错误不会输出给用户;
  • 根据实情,选择何时使用成熟的ajax库,何时自己编写底层的ajax代码。

请求数据

有五种常用的技术,用于向服务器请求数据,其中前三种比较常见,后两种(iframe/comet)一般在极端的情况下使用:

  • XMLHttpRequest (XHR)
  • Dynamic script tag insertion (动态脚本注入)
  • Multipart XHR(基本原理同xhr)
  • iframes
  • Comet (基于HTTP长连接的“服务器推”技术,服务器端推送,事件驱动,类nodejs,一直把这个和java的tomcat搞混了:()

XMLHttpRequest(XHR)

XHR允许一步发送和接收数据,可以指定和读取头信息,并且监听请求的状态,达到精确控制数据的请求和接收,但是不能跨域请求数据。

var url = '/data.php'; //请求地址
var params = [ //参数
  'id=934875',
  'limit=20'
];
var req = new XMLHttpRequest(); // 创建XHR(标准方式)
req.onreadystatechange = function() {
  if (req.readyState === 4) { // 所有数据接受完毕
    var responseHeaders = req.getAllResponseHeaders(); // 获取响应头信息
    var data = req.responseText; // 获取数据
    // 处理数据
  }
}
req.open('GET', url + '?' + params.join('&'), true);
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // 设置请求的头信息
req.send(null); // 发送请求

readyState是XHR优化的关键,可以在readyState==3,即数据开始接收但未完成时(这就是流streaming),对接收到的数据提前进行处理,这也是Multipart XHR的基础。
if (req.readyState === 3) { // 数据开始接收,但未完成
  var dataSoFar = req.responseText;
  ...
}
else if (req.readyState === 4) { // 所有数据接受完毕
  var data = req.responseText;
  ...
}

仅仅请求数据时,使用get方式的XHR请求,可以缓存数据提高性能,只有请求的参数很长,超过2048字节(ie的url长度限制)时,才使用POST。

动态脚本注入(Dynamic script tag insertion)

这种方法也叫JsonP,可以实现跨域请求数据,但是必须通过回调函数和服务器端返回的数据进行对接,把数据直接作为一个参数传入,无需对数据进行预处理;这样不能读取和设置头信息,只能通过get的方式发送请求,也不能监听请求的状态,引入无法控制的外部数据时需要小心处理。

//在客户端创建异步请求
var scriptElement = document.createElement('script');
scriptElement.src = 'http://any-domain.com/javascript/lib.js';
document.getElementsByTagName('head')[0].appendChild(scriptElement);

//在客户端定义回调函数
function jsonCallback(jsonString) {
var data = eval('(' + jsonString + ')');
// Process the data here...
}

//在服务器端把数据封装在回调函数中

http://any-domain.com/javascript/lib.js

jsonCallback({ "status": 1, "colors": [ "#fff", "#000", "#ff0000" ] });

Multipart XHR

MXHR允许在一个异步请求中传输多个文件(限于文本,js、css和base64图片),过程和XHR类似,只是在服务器端对数据进行了转换和合并(用约定字符串分隔),在客户端对接收到的数据进行监听和处理,这能够减少页面请求数,提升性能。

MXHR依赖定时器和分隔符,简单过程请看下面的代码,健壮的MXHR和性能测试可参考

var req = new XMLHttpRequest();
var getLatestPacketInterval, lastLength = 0;
req.open('GET', 'rollup_images.php', true);
req.onreadystatechange = readyStateHandler;
req.send(null);
function readyStateHandler{
  // 数据开始接收之后用定时器进行轮询
  if (req.readyState === 3 && getLatestPacketInterval === null) {
  getLatestPacketInterval = window.setInterval(function() {
    getLatestPacket();
  }, 15);
}
//数据接收完成之后 停止轮询并处理最后一个数据包
if (req.readyState === 4) {
  clearInterval(getLatestPacketInterval);
  getLatestPacket();
  }
}

function getLatestPacket() {
  var length = req.responseText.length;
  var packet = req.responseText.substring(lastLength, length);
  processPacket(packet);//在此根据约定字符串来分隔和处理返回的数据
  lastLength = length;
}

MXHR的缺点是不能缓存;且依赖于readyState==3和base64,ie6、7需要单独处理。

服务器端用php来合并资源(图片)

//读取图片并转化为base64编码的字符串
$images = array('kitten.jpg', 'sunset.jpg', 'baby.jpg');
foreach ($images as $image) {
  $image_fh = fopen($image, 'r');
  $image_data = fread($image_fh, filesize($image));
  fclose($image_fh);
  $payloads[] = base64_encode($image_data);
}
// 合并成一个字符串输出
$newline = chr(1); // 该字符不会出现在任何base64字符串中
echo implode($newline, $payloads);

客户端取得字符串之后转化为可用资源

function splitImages(imageString) {
  var imageData = imageString.split("\u0001");
  var imageElement;
  for (var i = 0, len = imageData.length; i < len; i++) {
    imageElement = document.createElement('img');
    imageElement.src = 'data:image/jpeg;base64,' + imageData[i];
    document.getElementById('container').appendChild(imageElement);
  }
}

function handleImageData(data, mimeType) {
  var img = document.createElement('img');
  img.src = 'data:' + mimeType + ';base64,' + data;
  return img;
}
function handleCss(data) {
  var style = document.createElement('style');
  style.type = 'text/css';
  var node = document.createTextNode(data);
  style.appendChild(node);
  document.getElementsByTagName('head')[0].appendChild(style);
}
function handleJavaScript(data) {
  eval(data);
}

发送数据

XHR发送数据

XHR发送数据和请求数据同样处理,使用POST方式发送数据时,至少会发送2个数据包。一个头信息、一个正文,而get方式只会发送正文一个数据包。

function xhrPost(url, params, callback) {
  var req = new XMLHttpRequest();
  req.onerror = function() {
    setTimeout(function() {
      xhrPost(url, params, callback);
    }, 1000);
  };
  req.onreadystatechange = function() {
    if (req.readyState == 4) {
      if (callback && typeof callback === 'function') {
        callback();
      }
    }
  };
  req.open('POST', url, true);// POST时需要注意header的处理
  req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  req.setRequestHeader('Content-Length', params.length);
  req.send(params.join('&'));
}

信标 Beacons

信标和脚本注入类似,只不过这里是创建一个image对象,把image的src设置为服务器脚本的url,但是并不把image插入DOM,在你只需要发送少量数据时使用。

可以有简单的监听,如果不需要返回信息时,发送不带正文的204 no content状态码,来阻止客户端继续等待正文。

var url = '/status_tracker.php';
var params = [
  'step=2',
  'time=1248027314'
];
var beacon = new Image();
beacon.src = url + '?' + params.join('&');

//服务器返回图片的宽度值来表示服务器的状态
beacon.onload = function() {
  if (this.width == 1) {
    // 成功
  }
  else if (this.width == 2) {
    // 失败 继续尝试
  }
};
beacon.onerror = function() {
// 容错处理
};

数据格式

数据格式对性能的影响体现在2个方面:下载(体积)、解析,因此需要根据具体情况来选用适用的数据格式。

XML格式

XML数据的体积比较庞大,结构依赖性强,语法模糊,而且解析的花费也较大,在高性能ajax中没有立足之地。

标准XML

<?xml version="1.0" encoding='UTF-8'?>
<users total="4">
  <user id="1">
    <username>alice</username>
    <realname>Alice Smith</realname>
    <email>alice@alicesmith.com</email>
  </user>
  <user id="2">
    <username>bob</username>
    <realname>Bob Jones</realname>
    <email>bob@bobjones.com</email>
  </user>
</users>

简化的XML,用属性代替子标签

<?xml version="1.0" encoding='UTF-8'?>
<users total="4">
  <user id="1-id001" username="alice" realname="Alice Smith" email="alice@alicesmith.com" />
  <user id="2-id001" username="bob" realname="Bob Jones" email="bob@bobjones.com" />
</users>

XML数据解析

function parseXML(responseXML) {
  var users = [];
  var userNodes = responseXML.getElementsByTagName('users');
  for (var i = 0, len = userNodes.length; i < len; i++) {
    users[i] = {
      id: userNodes[i].getAttribute('id'),
      username: userNodes[i].getAttribute('username'),
      realname: userNodes[i].getAttribute('realname'),
      email: userNodes[i].getAttribute('email')
    };
  }
  return users;
}

JSON

JSON使用javascript对象和数组直接量,因此体积小,而且易于解析。http://www.json.org/

JSON数据

[
  { "id": 1, "username": "alice", "realname": "Alice Smith", "email": "alice@alicesmith.com" },
  { "id": 2, "username": "bob", "realname": "Bob Jones", "email": "bob@bobjones.com" }
]

解析JSON数据

function parseJSON(responseText) {
  return eval('(' + responseText + ')');
}

不推荐使用eval来处理,可用JSON.parse()替代,安全性和容错性更好。

简化版JSON

[
  { "i": 1, "u": "alice", "r": "Alice Smith", "e": "alice@alicesmith.com" },
  { "i": 2, "u": "bob", "r": "Bob Jones", "e": "bob@bobjones.com" }
]

数组JSON

[
  [ 1, "alice", "Alice Smith", "alice@alicesmith.com" ],
  [ 2, "bob", "Bob Jones", "bob@bobjones.com" ]
]

解析数组json

function parseJSON(responseText) {
  var users = [];
  var usersArray = eval('(' + responseText + ')');
  for (var i = 0, len = usersArray.length; i < len; i++) {
    users[i] = {
      id: usersArray[i][0],
      username: usersArray[i][1],
      realname: usersArray[i][2],
      email: usersArray[i][3]
    };
  }
  return users;
}

数组JSON体积和解析都是最好的,但是数据的可读性变差了,也更加脆弱,对数据结构和具体内容要求更严谨。

JSON-P

JSON填充(JSON with padding),简称JSON-P,即把JSON数据作为参数,封装在一个回调函数里,数据是原生javascript,无需解析,不需要像JSON数据需要先JSON.parse或者eval处理,因此解析速度可以达到XHR的10倍,动态脚本注入结合JSON-P是高性能Ajax的基础。

parseJSON([
  { "id": 1, "username": "alice", "realname": "Alice Smith", "email": "alice@alicesmith.com" },
  { "id": 2, "username": "bob", "realname": "Bob Jones", "email": "bob@bobjones.com" }
]);

不要把敏感的数据放在JSON-P中,因为任何网站都可以调用这些数据,无法完全控制。

HTML

如果返回的数据是HTML格式的,在服务器端把数据转化为HTML,这比在客户端用javascript解析更快,那么页面就无需解析,直接用innerHTML插入到页面对应位置即可。

<ul class="users">
  <li class="user" id="1-id002">
    <a href="http://www.site.com/alice/" class="username">alice</a>
	<span class="realname">Alice Smith</span>
	<a href="mailto:alice@alicesmith.com"class="email">alice@alicesmith.com</a>
  </li>
  <li class="user" id="2-id002">
    <a href="http://www.site.com/bob/" class="username">bob</a>
	<span class="realname">Bob Jones</span>
	<a href="mailto:bob@bobjones.com" class="email">bob@bobjones.com</a>
  </li>
</ul>

HTML格式的数据比较臃肿,结构更复杂,调整结构时需要在服务器端修改,和json格式相反,HTML在下载中耗时较多,页面解析时消耗的CPU会减少,但还是需要较长时间来解析,其性能与其他格式无法直接比较,因此只有在极端情况下推荐使用。

自定义格式

理想的数据应该只包含尽量少的结构,同时可以识别出其中的每一个独立字段,对于非常大的数据这种格式是最快的,在解析速度和加载时间上。

1:alice:Alice Smith:alice@alicesmith.com;
2:bob:Bob Jones:bob@bobjones.com

function parseCustomFormat(responseText) {
  var users = [];
  var usersEncoded = responseText.split(';');
  var userArray;
  for (var i = 0, len = usersEncoded.length; i < len; i++) {
    userArray = usersEncoded[i].split(':');
    users[i] = {
      id: userArray[0],
      username: userArray[1],
      realname: userArray[2],
      email: userArray[3]
    };
  }
  return users;
}

然后通过split或循环split来处理数据,在javascript中split和循环处理这些字符串数据是非常快的。这里分隔符的选用是最重要的,理想情况下,她应该是一个单字符,而且不会在数据中出现。

PHP中使用ASCII字符示例:

function build_format_custom($users) {
  $row_delimiter = chr(1); //  \u0001  javascript中的Unicode编码
  $field_delimiter = chr(2); // \u0002 javascript中的Unicode编码
  $output = array();
  foreach ($users as $user) {
    $fields = array($user['id'], $user['username'], $user['realname'], $user['email']);
    $output[] = implode($field_delimiter, $fields);
  }
  return implode($row_delimiter, $output);
}

split可以使用字符串和正则作为参数,如果数据中存在空白记录,使用字符串比较保险,如果用正则,在ie中,split会忽略紧挨着的2个分隔符中的第二个分隔符,作者steven levithan有专门的解决方案

// 正则作为分割参数
var rows = req.responseText.split(/\u0001/);
// 正则作为分割参数(更安全)
var rows = req.responseText.split("\u0001");

Ajax性能优化

在选择了合适的传输方式和数据结构之后,可以继续从缓存来优化Ajax。

在服务器端设置HTTP头信息确保数据被浏览器缓存,然后使用get方式来获取数据,这是首选方案,可以跨页面和会话(sessions)。

PHP设置代码,缓存7天
$lifetime = 7 * 24 * 60 * 60; // 7天,以秒为单位
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT');

客户端把数据存储到本地,避免再次请求,这可以很好的控制缓存,在你需要更新缓存时可以删除缓存,此外移动设备上浏览器缓存很小时,这也是最佳选择。

var localCache = {};
function xhrRequest(url, callback) {
  // 检查请求是否已有本地缓存,有缓存则直接利用
  if (localCache[url]) {
    callback.success(localCache[url]);
    return;
  }
  // 如果没有缓存,发送请求
  var req = createXhrObject();
  req.onerror = function() {
    callback.error();
  };
  req.onreadystatechange = function() {
    if (req.readyState == 4) {
      if (req.responseText === '' || req.status == '404') {
        callback.error();
        return;
      }
      // 存储放回数据到本地缓存
      localCache[url] = req.responseText;
      callback.success(req.responseText);
    }
  };
  req.open("GET", url, true);
  req.send(null);
}
//必要时删除缓存
delete localCache['/user/friendlist/'];

Ajax库的局限

所有的javascript库都有自己的ajax对象,统一的接口,很好的兼容性。但是不能访问XMLHttpRequest的完整功能,比如MXHR中对readyState值为3的监听,直接操作XHR可以减少开销,但是也会带来一下兼容性问题。

function createXhrObject() {
  var msxml_progid = [
    'MSXML2.XMLHTTP.6.0',
    'MSXML3.XMLHTTP',
    'Microsoft.XMLHTTP', // 不支持readyState 3.
    'MSXML2.XMLHTTP.3.0', // 不支持readyState 3.
  ];
  var req;
  try {
    req = new XMLHttpRequest(); // 先尝试标准方法
  }
  catch(e) {
    for (var i = 0, len = msxml_progid.length; i < len; ++i) {
      try {
        req = new ActiveXObject(msxml_progid[i]);
        break;
      }
      catch(e2) { }
    }
  }
  finally {
    return req;
  }
}
tips
  • 数据格式和传输技术的测试结果页:http://techfoolery.com/formats/
  • 用eval来处理XHR返回的json字符串是很危险的,尤其是数据来源不可控的时候;JSON.parse()方法是理想的选择,很多浏览器都开始原生支持,不支持的可以用这个json处理函数来处理

高性能Javascript【六】快速响应的用户界面

在第一章中,我们知道Javascript和用户界面更新在同一个进程(浏览器UI线程)中运行,因此同一时刻只能处理其中的一个操作,当Javascript执行时界面无法更新,反之,当界面更新时Javascript无法执行,因此管理好Javascript运行时间对于良好的用户体验非常重要。

  • 任何javascript任务都不应当执行超过100毫秒,过长的执行时间会导致UI更新出现明显的延迟,对用户体验产生负面影响。
  • javascript运行期间,浏览器对用户交互行为的反应存在差异,javascript的长时间运行,将导致用户体验变得混乱和脱节。
  • 定时器可以用来安排代码延迟执行,它可以把一个长时间运行的脚本拆分成一系列的小任务。
  • 在支持Web workers的新版浏览器中,可以用她在UI线程外部执行javascript代码,从而避免UI锁定。

浏览器UI线程

UI线程基于简单的一个队列系统,当线程空闲时,队列中的下一个任务就会被提取出来并执行,队列里的任务是javascript代码,或者UI更新(包括重绘和重排),这个进程中比较特殊的是,队列中的一个任务被执行时,可能导致一个或多个新的任务加入到队列中。

<html>
<head>
  <title>Browser UI Thread Example</title>
</head>
<body>
<button onclick="handleClick()">Click Me</button>
<script type="text/javascript">
function handleClick(){
  var div = document.createElement("div");
  div.innerHTML = "Clicked!";
  document.body.appendChild(div);
}
</script>
</body>
</html>

点击按钮,屏幕上显示一条消息,要实现这个交互,UI线程需要添加2个任务到队列中,一个是更新按钮UI到点击状态,一个是执行javascript,当UI线程空闲时,先更新UI,然后执行javascript,在javascript执行时,添加了一个元素到页面中,于是另一次UI更新被添加到队列中,然后伺机执行,最后的结果是:2个任务开始,执行3个任务结束。

UI线程处于空闲状态是最理想的,用户所有的交互行为都会立即响应UI更新,如果UI线程中有任务在运行的时候,同时有用户交互行为发生,不仅没有即时的UI更新,甚至新的UI任务都不会被创建并添加到队列中,即不响应用户的任何交互行为,这是需要避免的。

浏览器限制

浏览器限制了javascript任务的运行时间:调用栈大小限制(详见第四章)、长时间运行(long-running)脚本限制。长时间运行脚本限制有事也被称为长时间运行脚本定时器或失控脚本定时器,其基本原理是记录一个脚本的运行时间,在达到限制时终止她,并显示提示,这回对用户造成困惑,避免这一问题的最佳方法就是避免他们的产生。

运行了多长可以用2种方法来度量:语句数量(比如ie中是500万条)、执行时长(firefox是10秒、safari是5秒),各个浏览器略有不同,有些还可以自行设置。

多久才算太久 100毫秒

单个javascript操作话费的总时间不应该超过100毫秒,这个数字源自Robert Miller在1968的研究,Jakob Nielsen在他的《可用性工程》中也有提到,这个100毫秒并不随时代变化而变化。

Nielsen指出,如果超过100毫秒,用户会感觉到自己和界面失去联系,当javascript运行超过100毫秒时,UI无法更新,用户会感觉失去了对界面的控制。在更复杂的情况下,在javascript执行时点击按钮,按钮的状态不会更新,按钮上绑定的事件也不会立即被触发,出现假死或挂起,这些任务会被加入到队列中,在最初的javascript执行完成之后一次执行,而这段时间交互引发的UI更新会被自动跳过,因为页面中的动态部分会被优先考虑。因此在脚本运行期间点击一个按钮,将无法看到她被按下的状态,尽管他的绑定的onclick事件函数会被执行。

使用定时器让出时间片段

总有一些任务不可能在100毫秒内完成,这时最理想的方法是让出UI线程的控制权(让javascript暂停执行),让UI可以即时更新,然后再继续执行javascript,于是定时器派上了用场。

setTimeout创建的定时器只会执行一次,setInterval创建的定时器会周期性重复运行,但是她们接收的参数是一样的,执行函数和等待时间。

设置了定时器之后,定时器里的执行函数不会马上执行,而是等设置的等待时间(从setTimeout调用就开始计时)到了时,再把执行函数添加到UI队列,等UI队列空闲时再执行。定时器会造成UI线程当前的任务暂停,切换到下一个任务,并且重置所有相关的浏览器限制,包括长时间运行脚本计时器,调用栈也会被重置为0,这一特性使得定时器成为长时间运行javascript代码理想的跨浏览器解决方案。

需要注意的是,定时器里面的代码只有在创建定时器的函数本身执行完之后,才会被执行。下面的代码中,如果anotherMethod的执行超过了50毫秒,那么定时器里的代码会在onclick处理完之前加入UI队列,即使UI线程空闲,她也需要等onclick绑定的所有函数执行完成之后才会被执行,但是会在onclick执行完成之后马上执行,感觉不到有延迟。

var button = document.getElementById("my-button");
button.onclick = function(){
 oneMethod();
 setTimeout(function(){
 document.getElementById("notice").style.color = "red";
 }, 50);
 anotherMethod();
};

定时器的精度

javascript定时器的精度通常不太精准,相差大约几毫秒,因此定时器不可用于测量实际时间。windows系统中定时器精度为15(微软的文档里是15.6)毫秒,也就是说设置为非15毫秒的倍数的时候,会自动转换为0或者15的倍数。最小建议值为25毫秒(实际为15或者30),确保至少有15毫秒的延时,大多数的浏览器在延时小于或等于10毫秒时表现不太一致。

使用定时器处理数组

造成长时间运行脚本的一种起因是过长的循环,在第四章中已经介绍过相关的优化,如果还是不理想,可以使用定时器,前提是:处理过程不需要同步、数据不需要按顺序处理,下面是一个封装好的函数。

function processArray(items, process, callback){
 var todo = items.concat(); //拷贝副本
 setTimeout(function(){
 process(todo.shift());//取出并处理第一个数组成员,之后删除这个数组成员
 if (todo.length > 0){ //如果还有数组成员,继续另一个定时器重复上面的过程
 setTimeout(arguments.callee, 25);
 } else {
 callback(items);//处理完成执行回调函数
 }
 }, 25);
}

函数的用法,三个参数:需要处理的数组,单个数组成员的处理函数,循环完成之后的回调函数。

var items = [123, 789, 323, 778, 232, 654, 219, 543, 321, 160];
function outputValue(value){
 console.log(value);
}
processArray(items, outputValue, function(){ console.log("Done!"); });

分割任务

如果一个任务运行时间过长,可是检查是否可以拆分成一系列短时间内可完成的子任务,用定时器和上面处理数组的方法来处理这些子任务,前提是任务可以异步处理而且不会影响用户体验或报错。

function multistep(steps, args, callback){
 var tasks = steps.concat(); //拷贝副本
 setTimeout(function(){
 var task = tasks.shift();
 task.apply(null, args || []);//执行数组中的第一个任务,之后删除
 if (tasks.length > 0){ //还有任务则继续
 setTimeout(arguments.callee, 25);
 } else {
 callback();//处理完成执行回调函数
 }
 }, 25);
}

封装好的代码如上,下面是具体的应用,三个参数分别为:子任务函数组成的数组、每个子任务函数运行时需要的参数(数组形式)、完成之后的回调函数。

function saveDocument(id){
 var tasks = [openDocument, writeText, closeDocument, updateUI];
 multistep(tasks, [id], function(){
 alert("Save completed!");
 });
}

记录代码运行时间

有时,定时器每次只执行一个任务的效率并不高,可以在上面的代码里加入一个运行时间的检测机制,定时器每次一个任务,就检测一下耗时,如果没有达到耗时上限(这里建议100/2毫秒),则继续下一个任务,只有当耗时达到设定的上限时,才继续另一个定时器,这样可以避免把任务分解得过于零碎。

function timedProcessArray(items, process, callback){
 var todo = items.concat(); //拷贝副本
 setTimeout(function(){
 var start = +new Date(); //记录开始时间
 do {
 process(todo.shift());
 } while (todo.length > 0 && (+new Date() - start < 50));//在50毫秒,每执行玩一个任务,检测一下时间,有时间则继续下一个任务
 if (todo.length > 0){//超过50毫秒,且任务未完成,进入下一个定时器
 setTimeout(arguments.callee, 25);
 } else {
 callback(items);//处理完成执行回调函数
 }
 }, 25);
}

定时器与性能

为了解决过度使用定时器会造成负面影响,可以使用定时器队列来解决:确保同时只有一个定时器在运行,一个定时器执行完成之后再进入下一个定时器。Neil Thomas在gmail的mobile版本中对定时器的进行了测试,多个高频率(100-200毫秒)的定时器同时运行,对体验会有明显的影响。

最近ie10推出了setimmediate,这个比定时器更好,但是仅仅是IE10中的 msSetIntermediate()

http://www.nczonline.net/blog/2011/09/19/script-yielding-with-setimmediate/
var id = setImmediate(function(){
 //do something
});
setImmediate(function(doc, win){
 //do something
}, document, window);

Web Workers

Web Workers打破浏览器的UI单线程限制,她可以在浏览器UI线程之外运行javascript代码,不会占用UI线程的时间,而且每个Worker都在自己的线程中运行,不会相互影响,不过这样Worker也就不能访问浏览器的许多资源。

var worker = new Worker("code.js");

上面的代码创建了Worker之后,Worker中的code.js会异步下载,等code.js加载和执行完之后才启动此worker。

网页与Worker只加通过onmessage和postMessage通信

var worker = new Worker("code.js");
worker.onmessage = function(event){//网页接收worker返回的数据
 alert(event.data);
};
worker.postMessage("Nicholas");//网页发送数据给worker

Worker也是通过onmessage和postMessage与网页通信

//inside code.js
importScripts("file1.js", "file2.js");//加载外部文件
self.onmessage = function(event){
 self.postMessage("Hello, " + event.data + "!");
};

下面的代码用来处理一个很长的json字符串(很大,至少500毫秒才能解析完)。

页面创建worker

var worker = new Worker("jsonparser.js");
//worker返回处理好的结构化的json数据,并传递进来之后开始调用
worker.onmessage = function(event){
 var jsonData = event.data;//接收被解析好的结构化的json数据
 evaluateData(jsonData);//使用结构化的json数据
};
worker.postMessage(jsonText);//传入要处理的json字符串给worker

worker中负责解析json字符串

//inside of jsonparser.js
//接收到json字符串之后开始调用
self.onmessage = function(event){
 var jsonText = event.data;//通过event.data接收json字符串
 var jsonData = JSON.parse(jsonText);//处理json字符串,这个过程会很长,但是会在独立的线程中进行,不会影响UI线程
 self.postMessage(jsonData);//返回处理好的结构化的json数据
};

Web Workers可用于处理与浏览器UI线程无关的长时间运行脚本,但是仅限于firefox3.5+、chrome 5+、safari 5+等现代浏览器。

  • 编码/解码大字符串
  • 复杂数学运算(图像或视频处理)
  • 大数组排序

相关的参考资料:
http://www.nczonline.net/blog/2009/08/11/timed-array-processing-in-javascript/
http://ejohn.org/blog/web-workers/
http://www.nczonline.net/blog/2009/08/18/experimenting-with-web-workers/
http://www.slideshare.net/nzakas/high-performance-javascript-2011

tips:

  • ie会控制用户交互行为触发的javascript任务,因此可以识别连续2个的重复的动作,比如在在脚本运行是连续点击按钮4次,最终按钮绑定的onclick事件函数只会被调用2次。
  • setInterval和setTimeout几乎相同,只是setInterval会重复添加javascript任务到UI队列,他们的主要区别是,如果UI队列中已经存在由同一个setInterval创建的任务,那么后续的任务不会被添加到UI队列中。
  • 每个定时器的真是延时时间在很大程度上取决于具体情况。普遍来讲,最好用至少25毫秒,因为再小的延时对大多数UI更新来说不够用。
  • 使用定时器处理数组的副作用是,处理数组的总时长增加了。因为在每一个条目处理完之后,UI线程会空闲出来,并且在下一条目开始处理之前会有一段延时。尽管如此,这样可以避免锁定浏览器给用户带来的不好体验,这种取舍是有必要的。
  • safari4中实现的worker只允许postMessage()传递字符串。自从firefox3.5允许传到序列化数据后,相应的规范也进行了更新,现在最新版本的safari和chrome都开始支持。

高性能Javascript【五】字符串和正则表达式

这一章的作者是 steven levithan,可以去他的blog上看相关的文章

  1. 当连接大量或很长的字符串时,join数组成员的方法是ie7以及以下的版本中性能最高的,但是在其他的现代浏览器中,join数组成员是最慢的,推荐使用简单的+和+=替代,同时要避免不必要的中间字符串。
  2. 回溯既是正则表达式匹配功能的基本组成部分,也是她的低效之源,要谨慎。
  3. 在正则表达式匹配某个字符串的时候,如果回溯失控,本来应该快速匹配的正则表达式,会变得很慢甚至导致浏览器崩溃;避免回溯失控的方法有:使相邻的字元互斥、避免嵌套量词对同一个字符串的相同部分进行多次匹配、通过重复利用向前查看(lookahead)的原子组(atomic groups)特性去除不必要的回溯。
  4. 提高正则表达式效率的各种技术手段有助于正则更快的匹配,并在非匹配位置上花更少的时间(详见下文提高正则表达式效率的方法)。
  5. 正则表达式并不总是最好的工具,尤其是当你只想搜索一个特定的字符串的时候。
  6. 清除字符串首尾空白的方法很多,一个简洁并且跨浏览器的方法是:使用2个简单的正则(一个用于头部,一个用于尾部),从尾部开始;循环向前搜索第一个非空白字符,或者把这个方法和正则结合起来使用,是另一个更好的替代方案,不会受到字符串长度的影响。

字符串连接

+/+=操作符连接

str = str + “one” + “two”在现代浏览器中性能更好;str += “one” + “two”在ie7及以下版本性能更好。

str += "one" + "two";

这是常用的连接字符串的方法,她运行的时候会经历下面四个步骤:

  1.  在内存中创建一个临时字符串;
  2. 连接后的”onetwo”被赋值给这个临时字符串;
  3. 临时字符串与str的当前值连接;
  4. 连接后的结果赋值给str。

下面的2行代码可以避免产生临时字符串(上面的第一、二步),在大多数浏览器中会提速10%-40%。

str += "one";
str += "two";

用一行代码也可以达到同样的效果。

str = str + "one" + "two";
//等价于 str = ((str + "one") + "two")

在这里str本身代替的临时字符串的作用,如果后面的这个str不再第一的位置的话(str = “one” + str + “two”),是达不到优化的效果的。这个和浏览器合并字符串是分配内存的方法有关:除ie外,现代浏览器会尝试为表达式左侧的字符串分配更多的内存,然后简单的把第二个字符串拷贝到她的尾部。在循环中,如果基础字符串位于最左侧,就可以避免重复拷贝一个不断变大的基础字符串。

在ie8中,连接字符串的时候,只是对字符串的引用关系进行记录,只有最后的时刻,才会发生合并把各个字符串拷贝到一个”真正的”字符串中,因此上面的优化也是有一定效果的。

在ie7以及更早的版本中,每连接一对字符串,都要把它复制到一块新分配的内存中,而不是拷贝到第一个的尾部,这样上面的优化只会更慢,因为这样会多次复制大字符串(longstr+s1、longstr+s2)。

数组项join连接

Array.prototype.join方法适用于ie7及更早版本,避免了+和+=带来的不断增大的大字符串的重复拷贝,消耗时间答复减少,而且消耗时间和连接的字符串的数量由平方递增(+\+=)变为线性递增(join);在大部分的现代浏览器中join方法比+/+=等方法更慢。

var strs = [];
strs.push(str1);
strs.push(str...);
newStr = strs.join("");

concat连接

String.prototype.concat要避免使用,concat比+和+=稍慢,而且和ie7中的+/+=操作一样,存在重复拷贝大字符串的性能问题。

正则表达式优化

正则表达式工作原理,了解原理有助于更好的解决各种影响正则性能的问题。

  1. 编译:浏览器验证正则表达式对象,之后把她转换成原生代码程序;把正则对象赋值给一个变量,可以避免重复编译。
  2. 设置起始位置:目标字符串的起始搜索位置,一般是字符串的其实字符、或者正则的lastIndex属性指定位置(限于带有/g的exec和test)、或者从第四步返回时的最后一次匹配的字符的下一个字符。
    浏览器优化正则表达式引擎的办法是,在这一阶段中通过早期预测跳过一些不必要的工作。例如,如果一个正则表达式以^开头,IE 和Chrome通常判断在字符串起始位置上是否能够匹配,然后可避免愚蠢地搜索后续位置。另一个例子是匹配第三个字母是x的字符串,一个聪明的办法是先找到x,然后再将起始位置回溯两个字符。
  3. 匹配每个正则表达式字元:从字符串的起始位置开始,逐个检查文本和正则模式,当一个特定的字元匹配失败时,回溯到之前尝试匹配的位置,尝试其他可能的路径。
  4. 匹配成功或失败:如果在当前的字符串位置有一个完全匹配,则宣布匹配成功;如果当前位置没有所有可能的路径都没有成功匹配,会退回第二步,重新设置起始位置,开始另一轮匹配…直到以最后一个字符串为其实位置,仍未成功,则宣布匹配失败。

理解回溯(Backtracking)

回溯是匹配过程的基本组成部分,是正则如此强大的根源,也是正则的性能消耗所在,因此如何减少回溯是提高正则的关键所在。回溯一般在分支和重复的情况下出现:

分支与回溯

/h(ello|appy) hippo/.test("hello there, happy hippo");
  1. 正则开始的h与字符串起始位置的h匹配,接下来的分支,按从左到右的原则,(ello|appy)中的ello先尝试匹配,字符串h后面也是ello,匹配成功,于是继续匹配正则中(ello|appy)之后的空格,仍然匹配成功,继续匹配正则中空格之后的h,字符串空格之后是t,匹配失败。
  2. 回到正则的分支(ello|appy)(这就是回溯),尝试用appy对字符串第一位个字符h之后的字符进行匹配,失败,这里没有更多的选项,不再回溯。
  3. 第一个起始位置匹配失败,起始位置后延一位,重新匹配h…直到字符串起始位置为14时,匹配到h。
  4. 于是开启新一轮的字元匹配,进入分支(ello|appy)中的ello,匹配失败。
  5. 回到正则的分支(ello|appy)(再次回溯),appy匹配成功,退出分支,匹配后续的 hippo,匹配字符串happy hippo,匹配成功,结束匹配。

重复与回溯

var str = "<p>Para 1.</p><img src='smiley.jpg'><p>Para 2.</p><div>Div.</div>";
/<p>.*<\/p>/i.test(str);
  1. 正则开始的<p>与字符串起始位置的<p>匹配,接下来是.*(.匹配换行以外任意字符,*是贪婪量词,表示重复0次或多次,匹配尽可能多的次数),.*匹配后续一直到字符串尾部的所有字符。
  2. 尝试匹配正则中.*后面的<,在字符串最后匹配失败,然后每次向前回溯一个字符尝试匹配…,直到</div>的第一个字符匹配成功,接下来正则中的\/也与字符串中的/匹配成功,继续匹配正则中的p,匹配失败,返回</div>,继续向前回溯,直到第二段的</p>,匹配成功,返回<p>Para 1.</p><img src=’smiley.jpg’><p>Para 2.</p>,里面有2个段落和一张图片,结束匹配。但这个可能并不是我们真正需要的结果,我们需要的可能是一个单一的段落。

贪婪与惰性

我们可以通过把贪婪量词*改成惰性量词*?来匹配单个段落/<p>.*?<\/p>/i.test(str)。惰性量词回溯的方式与贪婪连词的相反,当匹配到.*?时,她会先尝试匹配尽可能少的次数(0次),直接进入正则接下来的匹配部分<\/p>,但是字符串的<p>后面并没<,于是进行回溯,尝试对.*?进行一次重复匹配,仍然不行,继续回溯,两次重复匹配…直到找到最近的</p>,完成匹配,返回<p>Para 1.</p>。

参考:http://blog.stevenlevithan.com/archives/greedy-lazy-performance

回溯失控

回溯失控的时候,可能导致浏览器假死数秒、数分钟或更长时间,以下面这个正则为例:

/<html>[\s\S]*?<head>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/

匹配结构完整的html文件时,一切正常,但是有些标签缺失时问题就出现了:假如html文件最后的</html>缺失,最后一个[\s\S]*?重复会扩展到字符串末尾,匹配</html>失败,正则会依次向前搜索可以继续回溯的位置,在这里是<\/body>之前的倒数第二个[\s\S]*?,用它匹配到第一个</body>之后,继续向后扩展(惰性量词不是重复最少次数),查找第二个</body>,直到末尾仍然没有匹配,于是继续回溯到倒数第三个[\s\S]*?,依次类推下去…没有意义的回溯不断扩展开来,会消耗掉很多的资源。

回溯失控解决方案:具体化

比如用[^"\r\n]*替代.*?,可以避免回溯时.对”的匹配,同时避免超出预期的无效匹配,上面的例子可以改为。

/<html>(?:(?!<head>)[\s\S])*<head>(?:(?!<\/head>)[\s\S])*<\/head>(?:(?!<body>)[\s\S])*<body>(?:(?!<\/body>)[\s\S])*<\/body>(?:(?!<\/html>)[\s\S])*<\/html>/

以上通过重复一个非捕获组((?:(?!<head>)[\s\S]) 目标标签以外的任何字符)达到相同的效果:其中包含一个否定的向前查看(?!<head>)(排除目标标签),和[\s\S](任意字符)元序列,这样不但保证了非目标标签(非<head>字符)的匹配,并且保证了[\s\S]不会被扩展,但是这个优化是缺乏效率的,因为对每一个字符进行匹配时,都重复了一次向前查看。

回溯失控终极方案:模拟原子组(向前查看+反向引用):(?=([\s\S]*?<head>))\1。

/<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<\/head>))\2(?=([\s\S]*?<body>))\3(?=([\s\S]*?<\/body>))\4[\s\S]*?<\/html>/

原子组(向前查看)的任何回溯位置都会被丢弃,从根源上避免了回溯失控,但是向前查看不会消耗任何字符作为全局匹配的一部分,捕获组+反向引用在这里可以用来解决这个问题,需要注意的是这的反向引用次数,即上面的\1、\2、\3、\4对应的位置。

嵌套量词与回溯失控

嵌套量词(例如(x+)*)在匹配时,内部量词与外部量词的排列组合,会产生数量巨大分支路径,这在匹配失败之前会尝试所有的路径,这时候的消耗是巨大的。

最糟糕的情况是用/(A+A+)+B/来匹配10个A的字符串(非实际情况):

  1. 第一个A+匹配到10个A,回溯一个字符,第二个A+匹配到最后一个A,然后开始查找B,没有匹配
  2. 尝试所有的路径,第一个A+匹配到8个A,第二个A+匹配2个A…;或者分组(A+A+)+的重复中,第一个A+匹配到2个A,第二个A+匹配3个A…一共尝试2的10(字符串长度)次方1024次回溯
  3. 显示这个B不可能匹配到,但是正则会把所有可能的路径都尝试一遍,最后才宣布匹配失败。

要预防这种情况,需要确保表达式的2部分不能对字符串的相同部分进行匹配,/(A+A+)+B/可以优化为/AA+B/,或者用终极招式:模拟原子组(/((?=(A+A+))\2)+B/)来彻底消除回溯问题。

基准测试的说明

正则表达式的性能和她匹配的字符串紧密相关,结果差异也很大,因此需要根据具体的情况,用各种字符串来测试,包括不同的长度,不匹配的和近似匹配的。

更多提高正则表达式效率的方法

  • 让匹配更快失败,尤其是匹配很长的字符串时,匹配失败的位置要比成功的位置多得多。
  • 以简单、必须的字元开始,排除明显不匹配的位置,如锚点(^或$),特殊字符(x或\u263A)字符类([a-z]或\d之类的速记符),和单词边界(\b);尽量避免使用分组、选择、重复量词开头,如/one|two/、\s、\s{1,}等。
  • 使用量词模式时,尽量让重复部分具体化,让字元互斥,如用”[^"\r\n]*”代替”.*?”(这个依赖回溯)。
  • 减少分支数量、缩小分支范围,用字符集和选项组件来减少分支的出现,或把分支在正则上出现的位置推后,把分支中最常出现的情况放在分支的最前面。
    cat|bat -> [cb]at;
    red|read -> rea?d;
    red|raw -> r(?:ed|aw);
    (.|\r|\n) -> [\s\S]
  • 使用非捕获组,因为捕获组需要消耗时间和内存来记录反向引用,并不断更新,如果不需要反向引用,可用非捕获组(?:…)代替捕获组(…);当需要全文匹配的反向引用时,可用regex.exec()返回的结果或者在替换字符串是使用$&。
    此优化在firefox中效果较小,但其他浏览器中处理长字符串时有较大影响。
  • 精确匹配需要的文本以减少后续的处理,如果需要引用匹配的一部分,可使用捕获,然后通过反向引用来处理。
  • 暴露必需的字元,用/^(ab|cd)/而不是/(^ab|^cd)/。
  • 使用合适的量词,基于预期的回溯数量,使用合适的量词类型。
  • 把正则表达式赋值给变量以便复用和提升提升性能,这样可以让正则减少不必要的编译过程。
    while (/regex1/.test(str1)) {
    /regex2/.exec(str2);

    }
    用下面的代替上面的
    var regex1 = /regex1/,regex2 = /regex2/;
    while (regex1.test(str1)) {
    regex2.exec(str2);

    }
  • 将复杂的正则表达式拆分成简单的片段,每个正则只在上一个成功的匹配中查找,更高效,而且可以减少回溯。

何时不使用正则表达式

如果仅仅是搜索字符串,而且事先知道字符串的哪部分需要被测试时,正则并不是最佳的解决方案。比如,检查一个字符串是否以分号结尾:

/;$/.test(str);正则会从第一个字符开始,逐个测试整个字符串,看她是否是分号,在判断是否在字符串的最后,当字符串很长时,需要的时间越多。

str.charAt(str.length – 1) == “;”;这个直接跳到最后一个字符,检查是否为分号,字符串很小是可能只是快一点点,但是对于长字符串,长度不会影响所需的时间。

字符串的原生方法都是很快的,比如slice、substr、substring、indexOf、lastIndexOf等,他们可以避免正则带来的性能开销。

去除字符串首尾空白

String.prototype.trim = function() {
  var str = this.replace(/^\s+/, ""),
  end = str.length - 1,
  ws = /\s/;
  while (ws.test(str.charAt(end))) {
    end--;
  }
  return str.slice(0, end + 1);
}

这个解决方案用正则来去除头部的空白,位置锚^,会很快,主要是尾部的空白处理,像上面何时不使用正则表达式里说的,用正则并不是最佳的,这里用字符串原生方法结合正则来解决,可以避免性能受到字符串长度和空白的长度的影响。

说明:http://blog.stevenlevithan.com/archives/faster-trim-javascript

tips:

  • 由于正则表达式的全称”regular expression”有些长,经常使用”regex”替代。
  • 那些可以匹配任意字符的的字符集(如:[\s\S]、[\d\D]、[\w\W]或者[-\uFFFF])实际上等同于(?:.|\r|\n|\u2028|\u2029)。其中包含了点号和点号匹配不到的四个字符(回车、换行、行分隔符、段分隔符)。

高性能Javascript【四】算法和流程控制

Javascript代码执行时间大部分消耗在循环、条件判断、递归等语句中,这些也是其性能优化的要点。

  • for、while和do-while循环性能特性相似,所以没有一种循环类型明显快于或慢于其他类型。
  • 避免使用for-in循环,除非你需要遍历一个属性数量未知的对象。
  • 改善循环性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数。
  • 通常来说,switch总比if-else快,但并不总是最佳解决方案。
  • 在判断条件较多时,使用查找表比if-else和switch更快,查找表最快。
  • 浏览器的调用大小限制了递归算法在JavaScript中的应用;溢出错误会导致其他代码中断运行。
  • 如果你遇到溢出错误,可以将递归方法改为循环迭代算法,或使用Memoization来避免重复计算。

循环

Javascript中的四种循环:for循环、while循环、do-while循环、for-in循环。for、while和do-while循环性能特相差不大,其中只有for-in循环要明显慢一些(1/7),因为他需要同时搜索对象的实例和原型属性。因此,除非明确需要遍历属性数未知的对象,否则因避免使用for-in循环(用for循环或查找表代替)。

var props = ["prop1", "prop2"],i = 0;
while (i < props.length){
  process(object[props[i]]);
}

改善循环性能的最佳方式:减少每次迭代的运算量;减少每次循环的迭代次数。

减少每次迭代的运算量

//原始循环
for (var i=0; i < items.length; i++){
  process(items[i]);
}

每次循环体(迭代)需要执行的操作:

  1. 一次控制条件中的属性查询(items.length)
  2. 一次控制条件中的数值大小比较(i < items.length)
  3. 一次控制条件是否为true的比较(i < items.length == true)
  4. 一次自增操作(i++)
  5. 一次数组查找(items[i])
  6. 一次函数调用(process(items[i]))

1.用变量缓存需要的对象或者数组成员:减少对数组项或对象成员的查找次数,优化上面的第一步操作。(提升25%、ie中50%)

//减少属性查询
for (var i=0, len=items.length; i < len; i++){
process(items[i]);
}

2.颠倒数组的循环顺序,从后往前:减少比较次数,从2次比较(是否小于总数、是否为true)较少到一次(是否为true),优化了上面的第二步操作。(提升50%-60%)

//减少属性并颠倒顺序
for (var i=items.length; i--; ){
  process(items[i]);
}

现在每次循环体需要执行的操作变成了:

  1. 一次控制条件是否为true的比较(i == true)
  2. 一次自减操作(i++)
  3. 一次数组查找(items[i])
  4. 一次函数调用(process(items[i]))

每次循环减少了2步操作,当循环的数量级很大的时候,性能提升会很显著。

减少每次循环迭代的次数

一次数量级很大的循环的开销是比较大的,可以把一个大循环拆开成几个小循环,最常见的模式是“达夫设备(Duff’s Device.)”:把一个大的循环展开为几个小的循环,总的循环次数不变。

//Jeff Greenberg 在2/2001把Duff’s Device从C语言移植到Javascript中
var iterations = Math.floor(items.length / 8),
startAt = items.length % 8,
i = 0;
do {
switch(startAt){
case 0: process(items[i++]);
case 7: process(items[i++]);
case 6: process(items[i++]);
case 5: process(items[i++]);
case 4: process(items[i++]);
case 3: process(items[i++]);
case 2: process(items[i++]);
case 1: process(items[i++]);
}
startAt = 0;
} while (--iterations);

Duff’s Device把大循环分成每个迭代8次的小循环,8的余数再执行一次小循环。原理:在switch语句中,每个case不break的话,她后面的case都会被执行。这种方法在迭代次数超过1000以上时,效果比较明显。

假设一个800003次的循环,会被分成100001个小循环,第一个小循环迭代3次(余数为3,执行case 3、case 2、case 1),其余的100000次(总数除以8取整)每次迭代8次。

//Jeff Greenberg优化版
var i = items.length % 8;
while(i){
process(items[i--]);
}
i = Math.floor(items.length / 8);
while(i){
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
}

Duff’s device的各种优化版本性能比较http://jsperf.com/duffs-device,^位运算符,浮点数与整数(整数优)、除法与乘法的性能比较(乘法优)。

基于函数的迭代

foreach方法会先遍历成员,再在每个成员上执行一个函数,这是基于函数的迭代,因为要调用外部的函数方法,基于函数的迭代要慢(是循环迭代的1/8)。

items.forEach(function(value, index, array){
  process(value);
});

条件语句

if-else与switch

使用if-else还是switch?if-else:件数量很小时(2个或少数几个);件数量比较大时,switch(超过2个)。

  • 从易读性考虑:由条件数量决定,条件数量很小时,if-else容易读懂,当条件数量比较大时,switch语句更容易读懂。
  • 从性能考虑:switch比if-else运行要快,尤其是条件数量不断增大时,if-else的消耗要比switch多(1.大部分语言中的switch语句采用了分枝表索引优化,2.switch比较时使用全等操作符,不会发生类型转换的消耗)。

优化if-else

优化目标:用最小的条件判断次数,达到正确的分支。

方法:

1.把出现几率最多条件放在首位,按出现几率的大小顺序往后。

if (value < 5) {
//do something
} else if (value > 5 && value < 10) {
//do something
} else {
//do something
}

这个if-else,只有在value的值大部分情况下都小于5的时候,只需要一个条件判断,性能才是最优的。如果value的值为8的话,需要2次条件判断,消耗时间会增加。

2.if-else嵌套

if (value < 6){
  if (value < 3){
    if (value == 0){
      return result0;
    } else if (value == 1){
      return result1;
    } else {
      return result2;
    }
  } else {
  if (value == 3){
      return result3;
    } else if (value == 4){
      return result4;
    } else {
      return result5;
    }
  }
} else {
  if (value < 8){
    if (value == 6){
      return result6;
    } else {
      return result7;
    }
  } else {
    if (value == 8){
      return result8;
    } else if (value == 9){
      return result9;
    } else {
      return result10;
    }
  }
}

嵌套之后的语句,最多经过4次条件判断,就可以找到正确的分支,当value的值平均分布的时候,这样的嵌套可以节约大约50%的时间。

3.查找表

当有大量的离散值需要判读的时候,最好的方法避免使用if-else和switch,查找表(Lookup Tables)更快,Javascript中的查找表通过数组或对象的成员查询来实现,完全抛弃了条件判断。查找表的消耗和成员的多少没有关系,数量增大时几乎不会产生额外的开销;尤其是当条件判断中的value和对象成员之间存在对应关系时,查找表的优势最为突出:下例中的 results[value],不用进行任何判读,直接获取results的value成员。

//数组
var results = [result0, result1, result2, result3...]
//查找成员查找
return results[value];
//对象
var obj = {
num1:"value1",
num2:"value2",
num3:"value3",
...
}
//对象成员查找
return obj[value];

递归

最常见的递归就是阶乘了:

function factorial(n){
  if (n == 0){ //终止条件
    return 1;
  } else {
    return n * factorial(n-1);
  }
}

递归中最关键的是终止条件,还有浏览器的调用栈限制(Call Stack Limits),也就是说浏览器有最大的递归次数限制。因此递归的出问题的时候可以从终止条件和递归次数限制两个方面来分析和改善。

解决调用栈限制的方法,可以用循环迭代来替代递归,而且循环比迭代中反复运行一个函数开销要小,虽然有时候循环要比迭代慢一些,但这样可以避开调用栈限制。

function merge(left, right){
var result = [];
while (left.length > 0 && right.length > 0){
if (left[0] < right[0]){
  result.push(left.shift());
} else {
  result.push(right.shift());
}
}
return result.concat(left).concat(right);
}
//归并排列的递归实现
function mergeSort(items){
if (items.length == 1) {
  return items;
}
var middle = Math.floor(items.length / 2),
left = items.slice(0, middle),
right = items.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
//归并排列的循环实现
function mergeSort(items){
if (items.length == 1) {
  return items;
}
var work = [];
for (var i=0, len=items.length; i < len; i++){
  work.push([items[i]]);
}
work.push([]); //in case of odd number of items
for (var lim=len; lim > 1; lim = (lim+1)/2){
  for (var j=0,k=0; k < lim; j++, k+=2){
    work[j] = merge(work[k], work[k+1]);
  }
  work[j] = []; //in case of odd number of items
}
return work[0];
}

Memoization:利用一个缓存对象,缓存前一次的计算结果共后续计算使用,避免重复计算工作,提高速度。

function memfactorial(n){
if (!memfactorial.cache){
memfactorial.cache = {
  "0": 1,
  "1": 1
};
}
if (!memfactorial.cache.hasOwnProperty(n)){
  memfactorial.cache[n] = n * memfactorial (n-1);
}
return memfactorial.cache[n];
}
var fact6 = memfactorial(6);
//Memoization 封装
function memoize(fundamental, cache){
  cache = cache || {};
  var shell = function(arg){
    if (!cache.hasOwnProperty(arg)){
      cache[arg] = fundamental(arg);
    }
    return cache[arg];
  };
  return shell;
}
//阶乘
function factorial(n){
  if (n == 0){
    return 1;
  } else {
    return n * factorial(n-1);
  }
}
//Memoization 阶乘
var memfactorial = memoize(factorial, { "0": 1, "1": 1 });
var fact6 = memfactorial(6);

tips:

  1. 在for循环的初始化中定义一个新变量,等于在循环体外定义新变量,这个变量是函数级的,不是循环级别,这个变量会影响到循环体以外。
  2. 不要使用for-in来遍历数组成员,for-in会同时遍历数组成员以外的成员属性。
var a = ["one","two"];
a.name= "name";
for (p in a) {
  alert(a[p]);
}

此外Jeff Greenberg也有一篇关于Javascript优化的文章,推荐一下。http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html

CSS样式属性的层叠和属性值的计算及其优先级 (css specificity)

样式属性的层叠

决定哪些样式属性会生效。

样式有三个来源:作者(Author)、用户(User)、用户代理工具(User agent,比如浏览器),这三部分样式相互重叠、影响。

样式层叠时,会给每一条样式规则一个权重,当有好几条同样的规则时,权重高的会优先应用;一般情况下作者写的样式权重高于用户的,但是”!important”比较特殊,这是相反的:用户的要比作者的权重高,用户代理工具的权重最低。

样式层叠顺序

  1. 找出与目标元素匹配的所有样式声明
  2. 按照重要性和来源排序,按优先级升序
    用户代理工具(User agent)的样式声明
    用户正常(User)的样式声明

    作者正常(Author)的样式声明
    作者important(Author important)的样式声明
    用户important(User important)的样式声明
  3. 重要性和来源相同的,通过计算其选择器的特性(specificity of selector)来排序
  4. 最后,如果按照上面的规则排序之后还是一样的,那么按照样式声明出现的先后顺序来排序,后申明的优先级高。

不难看出,开发者可以控制的只有第3和第4两条,而第4条只有在特定的场合才会用到,这样第3条就是最关键的了。

样式属性值的计算

样式属性值被计算成可以被浏览器渲染的值的过程。

  1. 获取指定值(Specified values),以下顺序优先级递减:
    a.层叠而来的值(Cascade values、Specificity)
    b.继承下来的值(Inherited values)
    c.初始值(Initial value) http://www.iecss.com/
  2. 计算值(Computed values)
    URI转化为绝对路径,相对单位em、ex等转化为px或绝对长度,继承而来的属性值既是指定值又是计算值。
  3. 被使用值(Used values)
    相对值转化为绝对值(百分比的宽、高等,这些属性的绝对值依赖于容器渲染)
  4. 实际值(Actual values)
    被使用值的近似估算、取整等处理过程(因为边框不允许有小数,还有黑白与彩色的处理等)

选择器的特性权重(specificity of selector)

a:标签里有style属性 a=1,其他a=0
b:选择器中ID的个数
c:选择器中属性选择器和Class的个数
d:选择器中元素、伪元素选择器的个数

把四个值连起来a-b-c-d,组成选择器的特性权重

*             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

特性权重的比较

不同级别,高位(靠左)有值的,权重高(0,1,0,0高于0,0,80,100)
同一级别,数字大的,权重高(0,0,0,2高于0,0,0,1)。

原文地址:http://www.w3.org/TR/CSS2/cascade.html

Javascript随机排列数组 Random Array Sort

关于排序算法,有很多种,但是对于Javascript数组来说,她本身就有一个排序的方法sort,此外还有一个把数组顺序点到过来的方法reverse。

排序和颠倒 sort&reverse

var chars = ["am","i","here"];
var nums = [3,19,88,7,60];
chars.sort();  //am,here,i
nums.sort();  //19,3,60,7,88
chars.reverse();  //i,here,am

固定的比较函数

sort方法默认的排序规则是:按字母顺序对数组中的元素进行升序排列(字符编码的顺序)。这样的话就没办法对数字进行排序了,但是sort还有一个可选参数:比较函数。

var nums2 = [3,19,88,7,60];
function sortNum(a,b){
	return a - b
}
nums2.sort(sortNum);  //3,7,19,60,88

这个比较函数决定了排序的方式,上面代码中的比较函数实现的是数字的升序排序,排序时函数会先比较传入的两个数值,然后返回一个用于说明这两个数值的相对顺序的值。

如果这个值小于0,说明第一数值比第二个小,保持顺序不变,小的还是排在前面;如果这个值大于0,说明第一数值比第二个大,会改变顺序,把小的放到前面;依此类推,最后实现按从小到大排序。

反之,如果函数里面是b-a,则实现从大到小排序,需要注意比较函数中参数的顺序。

随机的比较函数

实现升序或者降序排列时,比较函数会按照固定的规则(a-b或b-a)来处理传入函数的的两个数值,然后根据返回值进行排序,返回的值和比较的数值是相关的

var nums3 = [3,19,88,7,60];
function randOrd(){
return (Math.round(Math.random())-0.5);
}
nums3.sort(randOrd);//19,7,60,88,3

如果比较函数中返回值和比较的数值是没有关系的,即比较函数没有固定的规则,同样的两个数值的比较,其返回值可能大于0,也可能不大于0,这样就不能保证每次比较都是大的排在前面,或者小的排前面。

这样出来的排序的结果,是随机的,即不能保证从大到小,也不能保证从小到大,而是一个随机的排序,可有时候,我们偏偏就是希望有这样一个结果:每次的顺序都不一样的随机序列,每个单项是不变的,变的只是顺序