传统上,网页中不会有大量的脚本,至少脚本很少会影响网页的性能。但随着网页越来越像 Web 应用程序,脚本的效率对网页性能影响越来越大。而且使用 Web 技术开发的应用程序现在越来越多,因此提高脚本的性能变得很重要。
对
于桌面应用程序,通常使用编译器将源代码转换为二进制程序。编译器可以花费大量时间优化最终二进制程序的效率。Web 应用程序则不同。因为 Web
应用程序需要运行在不同的浏览器、平台和架构中,不可能事先完全编译。浏览器在获得脚本后要执行解释和编译工作。用户要求不仅要求网页能快速的载入,而且
要求最终 Web 应用程序执行的效果要和桌面应用程序的一样流畅。Web 应用程序应能运行在多种设备上,从普通的桌面电脑到手机。
浏览
器并不很擅长此项工作。虽然 Opera 有着当前最快的脚本引擎,但浏览器有不可避免的局限性,这时就需要 Web 开发者的帮助。Web 开发者提高
Web 应用程序的性能的方法很多而且也很简单,如只需要将一种循环变成另一种、将组合样式分解成三个或者只添加实际需要的脚本。
本文从 ECMAScript/javascript
, DOM, 和页面载入方面分别介绍几种简单的能提高 Web 应用程序性能的方法。
目录
ECMAScript
- 避免使用
eval
或 Function
构造函数
- 重写
eval
- 如果你需要函数,那就用函数
- 避免使用
with
- 不要在影响性能的关键函数中使用
try-catch-finally
- 分隔
eval
和 with
- 避免使用全局变量
- 注意隐式对象转换
- 在关键函数中避免
for-in
- 优化 string 合并
- 基本运算符比函数调用更快
- 向
setTimeout()
和 setInterval()
传送函数名,而不要传送字符串
DOM
- 重绘和 reflow
- 减少 reflow 次数
- 最小化 reflow 影响
- 修改 DOM 树
- 修改不可见元素
- 测量大小
- 一次修改多个样式值
- 用流畅性换取速度
- 避免搜索大量节点
- 使用 XPath 提高速度
- 避免在遍历 DOM 时修改 DOM
- 使用变量保存 DOM 值
页面载入
- 避免保存来自其他文档的引用
- 快速历史浏览
- 使用 XMLHttpRequest
- 动态创建 SCRIPT 元素
-
location.replace()
控制历史项
ECMAScript
避免使用 eval
或 Function
构造函数
每次 eval
或 Function
构造函数作用于字符串表示的源代码时,脚本引擎都需要将源代码转换成可执行代码。这是很消耗资源
的操作 —— 通常比简单的函数调用慢100倍以上。
eval
函数效率特别低,由于事先无法知晓传给 eval
的字符串中的内容,eval
在其上下文中解释要处理的代码,也就是说编译器无法优化上下文,因此只能有浏览器在运行时解释代码。这对性能影响很大。
Function
构造函数比 eval
略好,因为使用此代码不会影响周围代码;但其速度仍很慢。
重写 eval
eval
不仅效率低下,而且绝大部分情况下完全没有使用的必要。很多情况下使用 eval 是因为信息以字符串形式提供,开发者误认为只有 eval 能使用此信息。下例是一个典型的错误:
1
|
function getProperty(oString) {
|
3
|
eval(
'oReference = test.prop.'
+oString);
|
下面的代码执行完全相同的函数,但没有使用 eval
:
1
|
function getProperty(oString) {
|
2
|
return
test.prop[oString];
|
在 Opera 9, Firefox, 和 Internet Explorer 中后者比前者快95%,在 Safari 中快85%。(注意此比较中不含函数本身调用时间。)
如果你需要函数,那就用函数
下面是常见的 Function
构造函数使用:
1
|
function addMethod(oObject,oProperty,oFunctionCode) {
|
2
|
oObject[oProperty] =
new
Function(oFunctionCode);
|
4
|
addMethod(myObject,
'rotateBy90'
,
'this.angle=(this.angle+90)%360'
);
|
5
|
addMethod(myObject,
'rotateBy60'
,
'this.angle=(this.angle+60)%360'
);
|
下面的代码没有使用 Function
构造函数,但提供了相同的功能:通过创建匿名函数:
1
|
function addMethod(oObject,oProperty,oFunction) {
|
2
|
oObject[oProperty] = oFunction;
|
4
|
addMethod(myObject,
'rotateBy90'
,function () {
this
.angle=(
this
.angle+
90
)%
360
; });
|
5
|
addMethod(myObject,
'rotateBy60'
,function () {
this
.angle=(
this
.angle+
60
)%
360
; });
|
避免使用 with
尽管看起来挺方便,但 with
效率很低。with
结构又创建了一个作用域,以便使用变量时脚本引擎搜索。这本身只轻微的影响性能。但严重的是编译时不知道此作用域内容,因此编译器无法像对其他作用域(如函数产生的作用域)那样对之优化。
另一个高效而且也挺方便的方法是使用变量引用对象,然后使用变量访问对象属性。但只有属性不是 literal type 时才适用,如字符串或布尔值。
考虑下面的代码:
1
|
with( test.information.settings.files ) {
|
4
|
tertiary =
'references'
;
|
下面的代码效率更高:
1
|
var testObject = test.information.settings.files;
|
2
|
testObject.primary =
'names'
;
|
3
|
testObject.secondary =
'roles'
;
|
4
|
testObject.tertiary =
'references'
;
|
不要在影响性能的关键函数中使用 try-catch-finally
try-catch-finally
结构比较特殊。和其他语法结构不同,它在 runtime 的当前作用域中创建新变量。每当 catch
执行时,就会将捕获到的 exception 对象赋给一个变量。这个变量不属于任何脚本。它在 catch
语句开始时被创建,在结束时被销毁。
由于此函数比较特殊,且是在运行时动态创建动态销毁,有些浏览器对其的处理并不高效。把 catch 语句放在关键循环中将极大影响性能。
如果可能,应在脚本中不频繁被调用的地方进行异常处理,或通过检查某种动作是否被支持来避免使用。下面的例子中,如果所需的属性不存在,将在循环语句中抛出许多异常:
1
|
var oProperties = [
'first'
,
'second'
,
'third'
,...,
'nth'
], i;
|
2
|
for
( i =
0
; i < oProperties.length; i++ ) {
|
4
|
test[oProperties[i]].someproperty = somevalue;
|
很多情况下,可把 try-catch-finally
结构移到循环外部。这样做稍微改变了程序语义,因为如果抛出异常,将停止整个循环:
1
|
var oProperties = [
'first'
,
'second'
,
'third'
,...,
'nth'
], i;
|
3
|
for
( i =
0
; i < oProperties.length; i++ ) {
|
4
|
test[oProperties[i]].someproperty = somevalue;
|
有时可用属性检测或其他检测代替 try-catch-finally
结构:
1
|
var oProperties = [
'first'
,
'second'
,
'third'
,...,
'nth'
], i;
|
2
|
for
( i =
0
; i < oProperties.length; i++ ) {
|
3
|
if
( test[oProperties[i]] ) {
|
4
|
test[oProperties[i]].someproperty = somevalue;
|
分隔 eval
和 with
因为 eval 和 with 结构严重影响性能,应该尽量避免使用这些结构。但如不得不使用时, 避免在频繁被调用的函数中或循环中使用这些结构。最好将这些结构放在只运行一次,或少量几次的代码中,并不要将其放在对性能要求较高的代码中。
如果可能,尽量将这些结构和其他代码分隔开,这样他们就不会影响脚本性能。如将其放在顶级函数中,或只执行一次然后保存运行结果,避免再次使用。
try-catch-finally
结构在一些浏览器中也会影响性能,包括 Opera ,因此最好也将其分隔。
避免使用全局变量
全局变量使用简单,因此很容易禁不住诱惑在脚本中使用全局变量。但有时全局变量也会影响脚本性能。
首先,如果函数或其他作用域内引用了全局变量,则脚本引擎不得不一级一级查看作用域直到搜索到全局作用域。查询本地作用域变量更快。
其次,全局变量将始终存在在脚本生命周期中。而本地变量在本地作用域结束后就将被销毁,其所使用的内存也会被垃圾收集器回收。
最后,window
对象也共享全局作用域,也就是说本质上是两个作用域而不是一个。使用全局变量不能像使用本地变量那样使用前缀,因此脚本引擎要花更多时间查找全局变量。
也可在全局作用域中创建全局函数。函数中可以调用其他函数,随着函数调用级数增加,脚本引擎需要花更多时间才能找到全局变量以找到全局变量。
考虑下面的简单例子,i
和 s
是全局作用域且函数使用这两个全局变量:
2
|
function testfunction() {
|
3
|
for
( i =
0
; i <
20
; i++ ) {
|
下面的函数效率更高。在大多数浏览器中,包括 Opera 9、最新版 Internet Explorer, Firefox, Konqueror 和 Safari,后者执行速度比上面代码快30%。
1
|
function testfunction() {
|
3
|
for
( i =
0
; i <
20
; i++ ) {
|
注意隐式对象转换
Literal,如字符串、数字和布尔值在 ECMAScript 中有两种表示方法。 每个类型都可以创建变量值或对象。如 var oString = 'some content';
, 创建了字符串值,而 var oString = new String('some content');
创建了字符串对象。
所
有的属性和方法都定义在 string 对象中,而不是 string 值中。每次使用 string 值的方法或属性, ECMAScript
引擎都会隐式的用相同 string 值创建新的 string 对象。此对象只用于此请求,以后每次视图调用 string 值方法是都会重新创建。
下面的代码将要求脚本引擎创建21个新 string 对象,每次使用 length
属性时都会产生一个,每一个 charAt
方法也会产生一个:
2
|
for
( var i =
0
; i < s.length; i++ ) {
|
下面的代码和上面相同,但只创建了一个对象,因此其效率更高:
1
|
var s =
new
String(
'0123456789'
);
|
2
|
for
( var i =
0
; i < s.length; i++ ) {
|
如果代码中常调用 literal 值的方法,你应像上面例子那样考虑创建对象。
注意本文中大部分技巧对于所有浏览器都有效,但此技巧特别针对于 Opera。此优化技巧在 Internet Explorer 和 Firefox 中改进效果没有在 Opera 中明显。
在关键函数中避免 for-in
for-in
常被误用,特别是简单的 for
循环更合适时。for-in
循环需要脚本引擎创建所有可枚举的属性列表,然后检查是否存在重复。
有时脚本已知可枚举的属性。这时简单的 for
循环即可遍历所有属性,特别是当使用顺序数字枚举时,如数组中。
下面是不正确的 for-in
循环使用:
2
|
for
( var i in oArray ) {
|
for
循环无疑会更高效:
2
|
var oLength = oArray.length;
|
3
|
for
( var i =
0
; i < oLength; i++ ) {
|
优化 string 合并
字符串合并是比较慢的。+
运算符并不管是否将结果保存在变量中。它会创建新 string 对象,并将结果赋给此对象;也许新对象会被赋给某个变量。下面是一个常见的字符串合并语句:
此代码首先创建临时string对象保存合并后的’xy’值,然后和a
变量合并,最后将结果赋给a
。下面的代码使用两条分开的命令,但每次都直接赋值给a
,因此不需要创建临时string对象。结果在大部分浏览器中,后者比前者快20%,而且消耗更少的内存:
基本运算符比函数调用更快
尽管单独使用效果不明显,但如果在需要高性能的关键循环和函数中使用基本运算符代替函数调用将可能提高脚本性能。例子包括数组的 push
方法,其效率低于直接在数组末位赋值。另一个例子是 Math
对象方法,大部分情况下,简单的数学运算符效率更高更合适。
1
|
var min = Math.min(a,b);
|
下面代码实现相同功能,但效率更高:
1
|
var min = a < b ? a : b;
|
向 setTimeout()
和 setInterval()
传送函数名,而不要传送字符串
setTimeout()
和 setInterval()
方法近似于 eval
。如果传进参数是字符串,则在一段时间之后,会和 eval
一样执行字符串值,当然其低效率也和 eval
一样。
但这些方法也可以接受函数作为第一个参数。在一段时间后将调用此函数,但此函数可在编译时被解释和优化,也就是说会有更好的性能。典型的使用 string 作为参数例子如下:
1
|
setInterval(
'updateResults()'
,
1000
);
|
2
|
setTimeout(
'x+=3;prepareResult();if(!hasCancelled){runmore();}'
,
500
);
|
第一个语句可以直接传递函数名。第二个语句中,可以使用匿名函数封装代码:
1
|
setInterval(updateResults,
1000
);
|
2
|
setTimeout(function () {
|
需要注意的是 timeout 或时间延迟可能并不准确。通常浏览器会花比要求更多的时间。有些浏览器会稍微提早完成下一个延迟以补偿。有些浏览器每次可能都会等待准确时间。很多因素,如 CPU 速度、线程状态和 javascript
负载都会影响时间延迟的精度。大多数浏览器无法提供1ms以下的延迟,可能会设置最小可能延迟,通常在10 和 100 ms之间。
DOM
通常主要有三种情况引起 DOM 运行速度变慢。第一就是执行大量 DOM 操作的脚本,如从获取的数据中建造新的 DOM 树。第二种情况是脚本引起太多的 reflow 或重绘。第三种情况是使用较慢的 DOM 节点定位方法。
第二种和第三种情况比较常见且对性能影响比较严重,因此先介绍前两种情况。
重绘(Repaint)和 reflow
重绘也被称为重画,每当以前不可见的元素变得可见(或反之)时就需要重绘操作;重绘不会改变页面布局。如给元素添加轮廓、改变背景颜色、改变样式。重绘对性能影响很大,因为需要脚本引擎搜索所有元素以确定哪些是可见的及哪些是应被显示的。
Reflow 是更大规模的变化。当 DOM 数被改变时、影响布局的样式被修改时、当元素的 className
属性被修改时或当浏览器窗口大小变化时都会引起 reflow。脚本引擎必须 reflow 相关元素以确定哪些部分不应被现实。其子节点也会被
reflow 以考虑其父节点的新布局。DOM 中此元素之后出现的元素也被 reflow
以计算新布局,因为它们的位置可能已被移动了。祖先节点也需要 reflow 以适应子节点大小的改变。总之,所有元素都需被重绘。
Reflow 从性能角度来说是非常耗时的操作,是导致 DOM 脚本较慢的主要原因之一,特别在手机等处理能力较弱的设备上。很多情况下,reflow 和重新布局整个网页耗时相近。
减少 reflow 次数
很多情况下脚本需要进行会引起 reflow 或重绘的操作,如动画就需要 reflow 操作,因此 reflow 是 Web 开发不可或缺的特性。为了让脚本能快速运行,应在不影响整体视觉效果的情况下尽量减少 reflow 次数。
浏
览器可以选择缓存 reflow 操作,如可以等到脚本线程结束后才 reflow 以呈现变化。Opera 可以等待足够数量的改变后才
reflow、或等待足够长时间后才 reflow、或等待脚本线程结束后才
reflow。也就是说如果一个脚本线程中的发生很多间隔很小的改变时,可能只引起一个 reflow 。但开发者不能依赖此特性,特别是考虑到运行
Opera 的不同设备的运算速度有很大差异。
注意不同元素的 reflow 消耗时间不同。Reflow 表格元素消耗的时间最多是 Reflow 块元素时间的3倍。
最小化 reflow 影响
正
常的 reflow 可能影响整个页面。reflow 的页面内容越多,则 reflow 操作的时间也越长。Reflow
的页面内容越多,需要的时间也就越长。位置固定的元素不影响页面的布局,因此如果它们 reflow 则只需 reflow
其本身。其背后的网页需要被重绘,但这比 reflow 整个页面要快得多。
所以动画不应该被用于整个页面,最好用于固定位置元素。大部分动画符合此要求。
修改 DOM 树
修改 DOM 树会
导致 reflow 。向 DOM 中添加新元素、修改 text 节点值或修改属性都可能导致 reflow。顺序执行多个修改会引起超过一个 reflow,因此最好将多个修改放在不可见的 DOM 树 fragment 中。这样就只需要一次 DOM 修改操作:
1
|
var docFragm = document.createDocumentFragment();
|
3
|
for
( var i =
0
; i < textlist.length; i++ ) {
|
4
|
elem = document.createElement(
'p'
);
|
5
|
contents = document.createTextNode(textlist[i]);
|
6
|
elem.appendChild(contents);
|
7
|
docFragm.appendChild(elem);
|
9
|
document.body.appendChild(docFragm);
|
也
可以在元素的克隆版本中进行多个 DOM 树修改操作,在修改结束后用克隆版本替换原版本即可,这样只需要一个 reflow
操作。注意如果元素中包含表单控件,则不能使用此技巧,因为用户所做修改将无法反映在 DOM
树种。此技巧也不应该用于绑定事件处理器的元素,因为理论上不应该克隆这些元素。
01
|
var original = document.getElementById(
'container'
);
|
02
|
var cloned = original.cloneNode(
true
);
|
03
|
cloned.setAttribute(
'width'
,
'50%'
);
|
05
|
for
( var i =
0
; i < textlist.length; i++ ) {
|
06
|
elem = document.createElement(
'p'
);
|
07
|
contents = document.createTextNode(textlist[i]);
|
08
|
elem.appendChild(contents);
|
09
|
cloned.appendChild(elem);
|
11
|
original.parentNode.replaceChild(cloned,original);
|
修改不可见元素
如果一个元素的 display
样式被设置为 none
,即使其内容变化也不再需要重绘此元素,因为根本就不会显示此元素。可以利用这一点。如果需要对一个元素或其内容做出多个修改,又无法将这些更改放在一个重绘中,则可以先将元素设置为 display
:none ,做出修改后,在把元素改回原来状态。
上面方法将导致两个额外的 reflow,一个是隐藏元素时另一个是重新显示此元素时,但此方法的总体效率仍较高。如果隐藏的元素影响滚动条位置,上面的方法也有可能会引起滚动条跳动。但此技术也被用于固定位置元素而不会引起任何不好看的影响。
1
|
var posElem = document.getElementById(
'animation'
);
|
2
|
posElem.style.display =
'none'
;
|
3
|
posElem.appendChild(newNodes);
|
4
|
posElem.style.width =
'10em'
;
|
6
|
posElem.style.display =
'block'
;
|
测量大小
如上面所述,浏览器可能会缓存多个修改一起执行,并只执行一次 reflow 。但注意为保证结果正确,测量元素大小也会引起 reflow 。尽管这不会造成任何重绘,但仍会在后台进行 reflow 操作。
使用 offsetWidth
这样的属性或 getComputedStyle
这样的方法都会引起 reflow 。即使不使用返回的结果,上述操作也会引起立即 reflow。如果重复需要测量结果,可以考虑只测量一次但用变量保存结果。
1
|
var posElem = document.getElementById(
'animation'
);
|
2
|
var calcWidth = posElem.offsetWidth;
|
3
|
posElem.style.fontSize = ( calcWidth /
10
) +
'px'
;
|
4
|
posElem.firstChild.style.marginLeft = ( calcWidth /
20
) +
'px'
;
|
5
|
posElem.style.left = ( ( -
1
* calcWidth ) /
2
) +
'px'
;
|
一次修改多个样式值
与 DOM 树修改相似,可将多个样式修改一次进行,以尽量减少重绘或 reflow数目。常见设置样式方法是逐个设置:
1
|
var toChange = document.getElementById(
'mainelement'
);
|
2
|
toChange.style.background =
'#333'
;
|
3
|
toChange.style.color =
'#fff'
;
|
4
|
toChange.style.border =
'1px solid #00f'
;
|
上面代码可能引起多次 reflow 和重绘。有两种改进方法。如果元素采用了多个样式,而且这些样式值事先知道,可以通过修改元素 class 使用新样式:
04
|
border: 1px solid #
000
;
|
09
|
border: 1px solid #00f;
|
12
|
document.getElementById(
'mainelement'
).className =
'highlight'
;
|
第二种方法是为元素定义新样式,而不是一个个赋值。这主要用于动态修改,如在动画中,无法事前知道新样式值。通过使用 style
对象的 cssText
属性,或者通过 setAttribute
. 可以实现此技巧。Internet Explorer 不允许第二种形式,支持第一种形式。有些较老的浏览器,包括 Opera 8 需要使用第二种形式,不支持第一种形式。最简单的方式是测试看是否支持第一种形式,如果支持就使用,如果不支持则使用第二种形式。
1
|
var posElem = document.getElementById(
'animation'
);
|
2
|
var newStyle =
'background: '
+ newBack +
';'
+
|
3
|
'color: '
+ newColor +
';'
+
|
4
|
'border: '
+ newBorder +
';'
;
|
5
|
if
( typeof( posElem.style.cssText ) !=
'undefined'
) {
|
6
|
posElem.style.cssText = newStyle;
|
8
|
posElem.setAttribute(
'style'
,newStyle);
|
用流畅性换取速度
作
为开发者,当然想要动画运行的越流畅越好,通常使用较小的时间间隔或较小的变化。如每10ms更新一次动画,或者每次移动1个像素。此动画可能在桌面电脑
上或某些浏览器中可以完美运行。但10ms时间间隔可能是浏览器使用100%CPU才能达到的最小值。有些浏览器甚至不能完成——要求每秒100个
reflow 对大部分浏览器来说都不容易。低性能的电脑或者其他设备可能无法达到此种速度,在这些设备上动画可能非常慢甚至失去响应。
因此最好暂时把开发者的骄傲放在一边,牺牲流畅性而换取速度。把时间间隔改为50ms或把动画步长设为5个像素,会消耗更少的计算资源
,在低性能设备上也能正常运行。
避免搜索大量节点
当需要查找节点时,尽量使用 DOM 内置方法和集合缩小搜索范围。如你想要定位某个包含某种属性的元素,可使用下面代码:
1
|
var allElements = document.getElementsByTagName(
'*'
);
|
2
|
for
( var i =
0
; i < allElements.length; i++ ) {
|
3
|
if
( allElements[i].hasAttribute(
'someattr'
) ) {
|
即使没听说过 XPath 这样的高级技巧,也可以看出上面的代码有两个问题导致速度变慢。首先它搜索每一个元素,而不是尝试缩小搜索范围。其次即使已经找到所需元素上卖弄代码还继续搜索。如果已知要找的元素在 id 为 inhere
的 div 中,最好使用下面的代码:
1
|
var allElements = document.getElementById(
'inhere'
).getElementsByTagName(
'*'
);
|
2
|
for
( var i =
0
; i < allElements.length; i++ ) {
|
3
|
if
( allElements[i].hasAttribute(
'someattr'
) ) {
|
如果已知要找元素是 div 的直接子节点,则下面的代码速度更快:
1
|
var allChildren = document.getElementById(
'inhere'
).childNodes;
|
2
|
for
( var i =
0
; i < allChildren.length; i++ ) {
|
3
|
if
( allChildren[i].nodeType ==
1
&& allChildren[i].hasAttribute(
'someattr'
) ) {
|
基本的思想就是尽量避免逐个查看 DOM 节点。DOM 有很多更好更快的方法,如 DOM 2 Traversal TreeWalker,效率要高于递归查找 childNodes
集合。
使用 XPath 提高速度
假如需要基于 H2-H4 元素在 html
网页中创建目录。在 html
中标题元素可以出现在很多地方,因此无法使用递归函数获取这些元素。传统 DOM 可能使用如下方法:
1
|
var allElements = document.getElementsByTagName(
'*'
);
|
2
|
for
( var i =
0
; i < allElements.length; i++ ) {
|
3
|
if
( allElements[i].tagName.match(/^h[
2
-
4
]$/i) ) {
|
若网页有超过2000个元素,此方法速度会很慢。如果支持 XPath,则可以使用一个快得多的方法,因为 XPath 查询引擎可比需被解释的 javascript
更好的被优化。在有些情况下,XPath 速度可能会快2个数量级以上。下面代码和上面完成一样的功能,但使用 XPath 因此速度要更快:
1
|
var headings = document.evaluate(
'//h2|//h3|//h4'
, document,
null
, XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null
);
|
3
|
while
( oneheading = headings.iterateNext() ) {
|
下面版本代码融合上述两种方法;在支持 XPath 的地方使用快速方法,在不支持时使用传统 DOM 方法:
01
|
if
( document.evaluate ) {
|
02
|
var headings = document.evaluate(
'//h2|//h3|//h4'
, document,
null
, XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null
);
|
04
|
while
( oneheading = headings.iterateNext() ) {
|
08
|
var allElements = document.getElementsByTagName(
'*'
);
|
09
|
for
( var i =
0
; i < allElements.length; i++ ) {
|
10
|
if
( allElements[i].tagName.match(/^h[
2
-
4
]$/i) ) {
|
避免在遍历 DOM 时修改 DOM
有些 DOM 集合是实时的,如果在你的脚本遍历列表时相关元素产生变化,则此集合会立刻变化而不需要等待脚本遍历结束。childNodes
集合和 getElementsByTagName
返回的节点列表都是这样的实时集合。
如果在遍历这样的集合的同时向其中添加元素,则可能会遇到无限循环,因为你不停的向列表中添加元素,永远也不会碰到列表结束。这不是唯一的问题。为提高性能,可能会对这些集合做出优化,如记住其长度、记住脚本中上一个访问元素序号,这样在你访问下一个元素时可快速定位。
如果你此时修改 DOM 树,即使修改的元素不在此集合中,集合还是会重新搜索以查看是否有新元素。这样就无法记住上一个访问元素序号或记住集合长度,因为集合本身可能已经变了,这样就无法使用优化:
1
|
var allPara = document.getElementsByTagName(
'p'
);
|
2
|
for
( var i =
0
; i < allPara.length; i++ ) {
|
3
|
allPara[i].appendChild(document.createTextNode(i));
|
下面的代码在 Opera 和 Internet Explorer 等主流浏览器中比上面代码快10倍以上。先创建一个要修改元素的静态列表,然后遍历静态列表并作出相应修改,而不是遍历 getElementsByTagName
返回的节点列表:
1
|
var allPara = document.getElementsByTagName(
'p'
);
|
3
|
for
( var i =
0
; i < allPara.length; i++ ) {
|
4
|
collectTemp[collectTemp.length] = allPara[i];
|
6
|
for
( i =
0
; i < collectTemp.length; i++ ) {
|
7
|
collectTemp[i].appendChild(document.createTextNode(i));
|
使用变量保存 DOM 值
有些 DOM 返回值无法缓存,每次调用时都会重新调用函数。如 getElementById
方法。下面是一个低效率代码的例子:
1
|
document.getElementById(
'test'
).property1 =
'value1'
;
|
2
|
document.getElementById(
'test'
).property2 =
'value2'
;
|
3
|
document.getElementById(
'test'
).property3 =
'value3'
;
|
4
|
document.getElementById(
'test'
).property4 =
'value4'
;
|
此代码为定位同一个对象调用了四次 getElementById
方法。下面的代码只调用了一次并将结果保存在变量中,单看这一个操作可能比上面单个操作要略慢,因为需要执行赋值语句。但后面不再需要调用 getElementById 方法!下面的代码比上面的代码要快5-10倍:
1
|
var sample = document.getElementById(
'test'
);
|
2
|
sample.property1 =
'value1'
;
|
3
|
sample.property2 =
'value2'
;
|
4
|
sample.property3 =
'value3'
;
|
5
|
sample.property4 =
'value4'
;
|
页面载入
避免保存来自其他文档的引用
如果文档访问过其他文档中的节点或对象,在脚本结束后避免保留这些引用。如果在全局变量或对象属性中保存过这些引用,通过设置为 null 清除之或者直接删除之。
原因是另一个文档被销毁后,如弹出窗口被关闭,尽管那个文档已经不再了,所有对那个文档中对象的引用都会在内存中保存整个 DOM 树和脚本环境。这也适用那些包含在frame,内联 frame,或 OBJECT 元素中的网页。.
1
|
var remoteDoc = parent.frames[
'sideframe'
].document;
|
2
|
var remoteContainer = remoteDoc.getElementById(
'content'
);
|
3
|
var newPara = remoteDoc.createElement(
'p'
);
|
4
|
newPara.appendChild(remoteDoc.createTextNode(
'new content'
));
|
5
|
remoteContainer.appendChild(newPara);
|
8
|
remoteContainer =
null
;
|
快速历史浏览(history navigation)
Opera
(和许多其他浏览器)
默认使用快速历史浏览。当用户点击后退或前进时,将记录当前页面的状态及页面中的脚本。当用户回到刚才的页面时,将立即显示刚才的页面,如同从没有离开此
页一样。不需要重新载入页面也不需要重新初始化。脚本继续运行,DOM
也和离开此页前完全相同。对用户来说这样反应很快,载入较慢的网页应用程序会有更好的性能。
尽管 Opera 提供开发者控制此行为的方式,最好还是尽量保持快速历史浏览功能。也就是说最好避免会影响此功能的动作,包括提交表单时禁用表单控件或让页面内容透明或不可见的渐出特效。
简单的解决方法是使用 onunload 监听器 reset 渐出效果或重新 enable 表单控件。注意对有些浏览器来说,如 Firefox 和 Safari,为 unload
事件添加监听器会禁用历史浏览。而在 Opera 中禁用提交按钮会导致禁用历史浏览。
1
|
window.onunload = function () {
|
2
|
document.body.style.opacity =
'1'
;
|
使用 XMLHttpRequest
此技巧不一定适用于每一个项目,但它能显著降低从服务器下载数据量,也能避免重载页面时销毁及创建脚本环境的开销。开始时正常载入页面,然后使用 XMLHttpRequest 下载最少量的新内容。这样 JavaScript 环境会一直存在。
注
意此方法也可能会导致问题。首先此方法完全破坏历史浏览。尽管可通过内联frame储存信息来解决此问题,但这显然不符合使用
XMLHttpRequest 的初衷。因此尽量少用,只在不需要回退到先前内容时使用。此方法还会影响辅助器具的使用( assistive
device),因为将无法察觉 DOM 已被更改,因此最好在不会引起问题的地方使用XMLHttpRequest。
若
JavaScript 不可用或不支持 XMLHttpRequest
则此技巧也会失效。最简单避免此问题的方法是使用正常链接指向新页面。增加一个检测链接是否被激活的事件处理器。处理器可以探测是否支持
XMLHttpRequest ,如果支持则载入新数据并阻止链接默认动作。载入新数据后,用其取代页面的部分内容,然后 request
对象就可以被销毁并允许垃圾收集器回收内存资源
。
01
|
document.getElementById(
'nextlink'
).onclick = function () {
|
02
|
if
( !window.XMLHttpRequest ) {
return
true
; }
|
03
|
var request =
new
XMLHttpRequest();
|
04
|
request.onreadystatechange = function () {
|
05
|
if
( request.readyState !=
4
) {
return
; }
|
06
|
var useResponse = request.responseText.replace( /^[/w/W]*<div id=
"container"
>|<//div>/s*<//body>[/w/W]*$/g ,
''
);
|
07
|
document.getElementById(
'container'
).innerHTML = useResponse;
|
08
|
request.onreadystatechange =
null
;
|
11
|
request.open(
'GET'
,
this
.href,
true
);
|
动态创建 SCRIPT 元素
加载和处理脚本需要时间,但有些脚本在载入后却从来未被使用。载入这样的脚本浪费时间和资源,并影响当前的脚本执行,因此最好不要引用这种不用的脚本。可以通过简单的加载脚本判断需要哪些脚本,并只为后面需要的脚本创建 script 元素。
理论上,这个加载脚本可在页面载入结束后通过创建 SCRIPT 元素加入 DOM。这在所有主流浏览器中都可以正常工作,但这可能对浏览器的提出更多的要求,甚至大于要载入的脚本本身。而且在页面载入之前可能就需要脚本,因此最好在页面加载过程中,通过 document.write
创建 script 标签。记住一定要转义‘/’字符防止终止当前脚本运行:
1
|
if
( document.createElement && document.childNodes ) {
|
2
|
document.write(
'<script type="text//javascript" src="dom.js"><//script>'
);
|
4
|
if
( window.XMLHttpRequest ) {
|
5
|
document.write(
'<script type="text//javascript" src="xhr.js"><//script>'
);
|
location.replace()
控制历史项
有时需要通过脚本修改页面地址。常见的方法是给 location.href
赋新地址。这将和打开新链接一样添加新历史项、载入新页面。
有时不想添加新历史项,因为用户不需要回到前面的页面。这在内存资源有限的设备中很有用。通过替换历史项恢复当前页面所使用的内存。可以通过 location.replace()
方法实现。
1
|
location.replace(
'newpage.html'
);
|
注意页面仍被保存在 cache 中,仍占用内存,但比保存在历史中要少的多。
作者 Mark ‘Tarquin’ Wilton-Jones · 2006年11月2日
本文翻译自 Efficient JavaScript
转自: http://www.woiweb.net/efficient-javascript.html
分享到:
相关推荐
独子棋demo.rar
云安全联盟软件定义边界SDP标准规范2.0202239页.pdf
Uniapp开发的微商个人相册多端小程序源码。使用 HBuilder X 导入本地项目,修改小程序AppID,以及Uni-app应用标识,调试发布即可。 小程序源码特点: 1、首页进行相册展示,采用分页 2、列表页面以文字形式进行分类,管理员可进行添加,修改和排序 3、每个列表下有多个相册,管理员可进行添加,修改和排序 4、每个相册有多张图片,有小图和大图模式进行切换 5、相册中可以长按图片进行选择删除和设为封面 6、相册可以进行分享 7、我的页面有管理员登录,联系客服等功能
内容概要:本文详细介绍了基于FPGA的144输出通道可切换电压源系统的设计与实现,涵盖系统总体架构、FPGA硬件设计、上位机软件设计以及系统集成方案。系统由上位机控制软件(PC端)、FPGA控制核心和高压输出模块(144通道)三部分组成。FPGA硬件设计部分详细描述了Verilog代码实现,包括PWM生成模块、UART通信模块和温度监控模块。硬件设计说明中提及了FPGA选型、PWM生成方式、通信接口、高压输出模块和保护电路的设计要点。上位机软件采用Python编写,实现了设备连接、命令发送、序列控制等功能,并提供了一个图形用户界面(GUI)用于方便的操作和配置。 适合人群:具备一定硬件设计和编程基础的电子工程师、FPGA开发者及科研人员。 使用场景及目标:①适用于需要精确控制多通道电压输出的实验环境或工业应用场景;②帮助用户理解和掌握FPGA在复杂控制系统中的应用,包括PWM控制、UART通信及多通道信号处理;③为研究人员提供一个可扩展的平台,用于测试和验证不同的电压源控制算法和策略。 阅读建议:由于涉及硬件和软件两方面的内容,建议读者先熟悉FPGA基础知识和Verilog语言,同时具备一定的Python编程经验。在阅读过程中,应结合硬件电路图和代码注释,逐步理解系统的各个组成部分及其相互关系。此外,实际动手搭建和调试该系统将有助于加深对整个设计的理解。
地级市政府通过制定相关政策来推动数字经济的发展和数字政府的建设。这些政策可能包括鼓励企业数字化转型、促进数字技术创新、加强数字基础设施建设、优化数字政务服务等方面的内容。政策制定的频率和力度,可以在一定程度上反映政府对数字领域的关注度。 在地级市政府数字关注度的背景下,词频分析成为了一种有效的工具,用以衡量政府文件和宣传资料中涉及数字技术和数字化转型相关词汇的频次,进而揭示政府对这一领域的关注程度和重视方向。 数据名称:地级市-政府数字关注度、词频
内容概要:本文详细探讨了在Android平台上进行图像模板匹配的技术挑战和解决方案,特别是在处理不同尺寸和旋转角度的目标物时的方法。文中介绍了使用OpenCV构建图像金字塔、处理旋转模板以及利用NEON指令集优化性能的具体实现。此外,文章还讨论了在armeabi-v7a和arm64-v8a这两种主要ARM架构下的优化技巧,如内存对齐、SIMD指令优化、RenderScript并行处理等。作者分享了许多实践经验,包括如何避免常见的性能瓶颈和兼容性问题。 适合人群:有一定Android开发经验,尤其是熟悉OpenCV和NDK编程的中级及以上开发者。 使用场景及目标:适用于需要在移动设备上进行高效图像识别的应用开发,如实时视频流中的物体检测、游戏内的道具识别等。目标是提高模板匹配的速度和准确性,同时确保在不同硬件配置下的稳定性和兼容性。 其他说明:文章提供了丰富的代码片段和实际案例,帮助读者更好地理解和应用所介绍的技术。特别强调了在不同ARM架构下的优化策略,为开发者提供了宝贵的参考资料。
内容概要:本文探讨了一种改进的粒子群优化(PSO)算法在微电网多目标优化调度中的应用。传统PSO在解决此类复杂问题时常陷入局部最优解,而改进版通过引入动态惯性因子和自适应变异操作,显著提升了算法性能。文中详细介绍了这两种改进措施的具体实现方法及其对算法收敛性和解质量的影响。此外,还展示了该算法在实际微电网调度任务中的表现,特别是在权衡经济成本与环境效益方面的能力。 适合人群:从事电力系统优化、智能电网研究的专业人士以及对进化算法感兴趣的学者和技术人员。 使用场景及目标:适用于需要进行高效能源管理的场合,如分布式发电系统的规划与运行。主要目的是寻找既能降低成本又能减少环境污染的最佳调度方案。 其他说明:文中提供了大量伪代码片段帮助读者理解具体的技术细节,并强调了参数调节对于最终结果的重要性。同时指出,该方法不仅限于微电网领域,还可以扩展应用于其他类型的优化问题。
Delphi 12.3控件之TeeChart Offline Keygen.7z
内容概要:本文详细介绍了如何利用MATLAB进行屈光度计算及其数据处理方法。首先解释了屈光度的基本概念和计算公式,接着展示了如何通过MATLAB代码读取、清理和转换焦距数据为屈光度,并进行了必要的单位转换。针对可能出现的异常值和噪声,文中提供了有效的数据清洗手段。此外,还探讨了如何对屈光度数据进行统计分析以及可视化呈现,如绘制趋势图和散点图等。最后,提到了将MATLAB代码转化为C++代码以便集成到硬件系统的高级应用。 适合人群:从事光学研究、眼科医疗设备开发的技术人员,以及对MATLAB有兴趣的学习者。 使用场景及目标:适用于需要精确处理和分析光学数据的研究机构或企业,旨在提高屈光度计算的效率和准确性,确保数据质量的同时优化实验结果。 其他说明:文中不仅涵盖了基本的操作步骤,还包括了许多实用的小贴士和技术细节,有助于读者更好地理解和掌握相关内容。同时强调了单位一致性的重要性,提醒开发者注意潜在的问题。
349421c2-4955-4132-b4da-808a3a171bfe.pdf
1744300906657718_download.jsp
【内容概要】 本文详细解析了企业筛选简历的“三重门”系统,包括ATS系统初筛、HR复核和业务部门终极评估三个阶段。首先,ATS系统作为关键词匹配引擎,强调了关键词的重要性及其优化方法;其次,HR在6秒内通过“薄片判断”评估简历的职业连贯性、成就量化和岗位匹配度;最后,业务部门则侧重于技术能力和文化适配性的综合评估。文章还揭示了各环节中的心理学原理和认知偏差,并提供了针对性的优化建议。 【适合人群】 正在求职或有求职打算的职场人士,尤其是希望提升简历通过率的求职者。 【使用场景及目标】 ①帮助求职者理解企业筛选简历的具体流程; ②提供简历优化的具体方法,如关键词优化、成就量化、案例准备等; ③指导求职者如何根据不同阶段的评审特点调整简历内容。 【其他说明】 文章结合了最新的招聘趋势研究报告和心理学理论,强调简历不仅是通过筛选的工具,更是展示个人能力和价值的平台。求职者应充分利用这些心理规律,打造更具吸引力的简历,为后续面试做好铺垫。
内容概要:本文详细介绍了使用PFC2D5.0进行二维岩石单轴压缩模拟的具体方法和代码实现。首先,通过设定模型的基本参数如颗粒生成、粘结设置、加载控制等,构建了一个完整的岩石样品模型。接着,深入探讨了加载过程中应力应变曲线的变化规律以及能量分析的方法,包括弹性应变能、动能和耗散能的监测。此外,还提供了裂隙统计的技术手段,能够精确捕捉岩石内部裂隙的发展情况。最后,强调了参数调整对模拟效果的影响,并给出了优化建议。 适合人群:从事岩土工程、地质力学研究的专业人士和技术爱好者。 使用场景及目标:适用于需要深入了解岩石力学特性的研究人员,帮助他们掌握PFC2D软件的应用技巧,提升科研能力。同时,也为相关领域的学生提供了一套实用的学习资料。 其他说明:文中提供的代码可以直接应用于PFC2D5.0环境,便于用户快速上手并进行实验验证。通过对不同参数的调整,可以模拟多种类型的岩石破坏行为,为实际工程项目提供理论支持。
内容概要:本文详细介绍了如何使用Fluent进行激光焊接的数值模拟,重点讲解了锥形高斯热源的建模方法。文章首先解释了锥形高斯热源的特点及其与普通高斯热源的区别,然后给出了具体的UDF代码实现,包括热源强度的计算、热流衰减的控制以及热源移动的实现。此外,还讨论了网格划分、材料参数设置、常见错误排查和优化技巧等方面的内容。通过实例和操作视频,帮助读者快速掌握激光焊接数值模拟的方法和技术要点。 适合人群:具有一定CFD基础并希望深入学习激光焊接数值模拟的研究人员和工程师。 使用场景及目标:适用于需要精确模拟激光焊接过程的研究项目或工业应用,旨在提高模拟精度,减少试验成本,优化焊接工艺参数。 其他说明:文中提供了大量实用的操作技巧和注意事项,如网格划分建议、材料参数选择、UDF代码调试等,有助于解决实际操作中可能遇到的问题。同时,附带的操作视频和GitHub上的完整案例包也为初学者提供了宝贵的学习资源。
序列化.md
"ResumePlatformFront 笔试面试全攻略与资源宝典"——一站式前端求职解决方案!精选高频笔试真题解析、大厂面试经验分享、实战项目模板及技能进阶指南,助你系统攻克前端求职难关。从简历优化到Offer谈判,覆盖求职全流程,配套免费资源库持续更新。无论应届生还是进阶开发者,这里都是你斩获心仪Offer的强力后盾!
weixin205微信小程序线上教育商城ssm(文档+源码)_kaic
内容概要:本文详细介绍了如何利用COMSOL软件构建岩石损伤与温度、渗流耦合的多物理场模型。首先解释了温度变化引起岩石膨胀/收缩以及渗流压力改变裂纹发展的物理机制,并通过PDE方程组进行描述。接着展示了具体的实现方法,如定义损伤变量、设置导热系数和渗透率随损伤变化的关系,以及引入温度修正的Mohr-Coulomb准则。文中还讨论了求解器配置技巧,强调了非线性收敛问题的解决方案。此外,作者分享了一些实际建模过程中遇到的问题及解决经验,如参数选择不当导致的模型发散等。 适合人群:从事岩土工程、地质工程及相关领域的研究人员和技术人员,特别是对多物理场耦合仿真感兴趣的学者。 使用场景及目标:适用于需要深入理解岩石在温度、渗流和应力共同作用下的损伤演化规律的研究项目。目标是帮助读者掌握COMSOL中多物理场耦合模型的建立方法,提高数值模拟的准确性。 其他说明:文章不仅提供了理论背景,还包括大量实用的代码片段和调试建议,有助于读者更好地理解和应用所学知识。
2023-04-06-项目笔记-第四百六十四阶段-课前小分享_小分享1.坚持提交gitee 小分享2.作业中提交代码 小分享3.写代码注意代码风格 4.3.1变量的使用 4.4变量的作用域与生命周期 4.4.1局部变量的作用域 4.4.2全局变量的作用域 4.4.2.1全局变量的作用域_1 4.4.2.462局变量的作用域_462- 2025-04-10
内容概要:本文详细介绍了基于滑膜观测器的永磁同步电机(PMSM)无传感器控制技术及其在MATLAB/Simulink中的仿真实现。首先阐述了PMSM的特点及其在现代工业中的重要地位,接着重点讲解了转子磁场定向矢量控制(FOC)的工作原理,特别是电流环的设计和电压解耦的作用。然后深入探讨了一阶滑膜观测器的实现方法,展示了如何通过电机的电压和电流信号估计转子位置和速度。最后,通过搭建完整的Simulink仿真模型并运行仿真,评估了控制策略的性能,并提供了配套的英文文献以供进一步研究。 适合人群:从事电机控制系统设计的研发工程师和技术爱好者,尤其是对无传感器控制技术和滑膜观测器感兴趣的读者。 使用场景及目标:适用于希望深入了解PMSM无传感器控制技术的工程师,旨在帮助他们掌握滑膜观测器的设计和实现,提高系统的可靠性和降低成本。同时,也为后续的实际应用和优化提供了理论依据和技术支持。 其他说明:文中提供的代码片段和仿真模型有助于读者更好地理解和实践相关技术,而配套的英文文献则为深入研究提供了宝贵的参考资料。