《高性能JavaScript》读书笔记(二)

第四章 算法和流程控制

代码的整体结构是执行速度的决定因素之一。性能损失与代码组织方式和具体问题解决办法直接相关。

循环
循环是最常用的模式之一,理解JavaScript中循环对性能的影响至关重要,因为死循环或者长时间的循环会严重影响用户体验。

循环的类型
ECMA标准定义了四种类型的循环。
第一个是标准的for循环。for循环是最常用的JavaScript循环结构,它由四部分组成:初始化体,前测条件,后执行体,循环体。for循环封装上的直接性是开发者喜欢的原因。

第二种循环是while循环。while循环是一个简单的预测试循环,由一个预测条件和一个循环体构成。任何for循环都可以写成while循环。

第三种循环类型是do-while循环。do-while循环是JavaScript中唯一一种后测试的循环,它包括两部分:循环体和后测试条件体。do-while循环中,循环体至少运行一次。

第四种循环称为for-in循环。此循环有一个非常特殊的用途:它可以枚举任何对象的命名属性,基本格式如下:

1
2
3
for(var prop in object){
    //loop body
}

每次循环执行,属性变量被填充以对象属性的名字(一个字符串),直到所有的对象属性遍历完成才返回。返回
的属性包括对象的实例属性和它从原型链继承而来的属性。

循环性能
在四种循环中,只有for-in循环比其他循环明显要慢。
因此推荐的做法是:除非你需要对数目不详的对象属性进行操作,否则避免使用for-in循环。如果迭代遍历一个有限的,已知的属性列表,使用如下模式:

1
2
3
4
5
var props = ["prop1","prop2"],i = 0;
while(i < props.length){
    process(object[props[i]]);
    i++;
}

其他三种循环性能相当,选择循环应基于需求而不是性能。考虑如下两个因素:

每次迭代干什么?
迭代的次数。
通过减少这两者中一个或全部(的执行时间),你可以积极地影响循环的整体性能。

减少迭代的工作量
限制在循环体内进行耗时操作的数量是一个加快循环的好方法。

在一个典型的数组处理循环中,每次运行循环体都要发生如下几个操作:
1.在控制条件中读一次属性(items.length);
2.在控制条件中执行一次比较(i < items.length);
3.比较操作,察看条件控制体的运算结果是否为true(i < items.length == true);
4.一次自加操作(i++);
5.一次数组查找(items[i]);
6.一次函数调用(process(items[i]))。

在简单的循环中,每次迭代也要进行许多操作。减少每次迭代中操作的总数可以大幅度提高循环整体性能。

优化循环工作量的第一步是减少对象成员和数组项查找的次数。例如,将数组的长度(items.length)赋值给一个局部变量,在控制条件中使用这个局部变量,从而提高循环的性能。
还可以通过改变数组元素的顺序提高循环性能。倒序循环是编程语言中常用的性能优化方法。

采用倒序循环后,每次迭代只进行如下操作:
1.在控制条件中进行一次比较(i == true);
2.一次减法操作(i–);
3.一次数组查询(items[i]);
4.一次函数调用(process(items[i]));

倒序循环的每次迭代中减少两个操作,随着迭代次数的增长,性能将显著提升。

减少迭代次数
即使循环体中最快的代码,累计迭代上千次(也将是不小的负担)。所以,减少循环的迭代次数可获得显著的性能提升。最广为人知的限制循环迭代次数的模式称作“达夫设备”。

达夫设备是一个循环体展开技术,在一次迭代中实际上执行了多次迭代操作。典型实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);

达夫设备背后的基本理念是:每次循环中最多可8次调用process()函数。循环迭代次数为元素总数除以8.
因为总数不一定是8的整数倍,所以startAt变量存放余数,指出第一次循环中应当执行多少次process();

此算法一个稍快的版本取消了switch表达式,将余数处理和主循环分开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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--]);
}

是否值得使用达夫设备,取决于迭代的次数,如果循环迭代次数少于1000次,它提升的性能微不足道,但迭代次数超过1000次,达夫设备的效率将明显提升。

基于函数的迭代
ECMA-262标准介绍了本地数组对象的一个新方法forEach()。此方法遍历一个数组的所有成员,并在每个成员上执行一个函数。在每个元素上执行的函数作为forEach()的参数传进去,并在调用时接收三个参数,它们是:数组项的值,数组项的索引和数组自身。

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

forEach()函数在Firefox,Chrome和Safari中为原生函数。另外在JavaScript库都有等价实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//YUI 3
Y.Array.each(items, function(value, index, array){
process(value);
});
//jQuery
jQuery.each(items, function(index, value){
process(value);
});
//Dojo
dojo.forEach(items, function(value, index, array){
process(value);
});
//Prototype
items.each(function(value, index){
process(value);
});
//MooTools
$each(items, function(value, index){
process(value);
});

基于函数的迭代更加便利,但还是比基于循环的迭代要慢一些。每一个数组项要关联额外的函数调用是造成速度慢的原因。
在所有情况下,基于函数的迭代占用时间是基于循环的迭代的八倍,因此在关注执行时间的情况下它并不是一个合适的方法。

条件表达式
与循环相似,条件表达式决定JavaScript运行流的走向。

if-else与switch比较
使用if-else或者switch的流行理论是基于测试条件的数量:条件数量较大,倾向于使用switch而不是if-else。
在大多数情况下switch表达式比if-else更快,但只有当条件数量很大时才明显更快。两者间的主要性能区别在于:
当条件体增加时,if-else性能负担增加的程度比switch更多。
一般来说,if-else适用于判断两个离散的值或者判断几个不同的值域。如果判断多于两个离散值,switch表达式会更好。

优化if-else
优化if-else的目标总是最小化找到正确分支之前所判断条件体的数量。最简单的优化方法是将最常见的条件体放在首位。if-else 中的条件体应当总是按照从最大概率到最小概率的顺序排列,以保证理论运行速度最快。

另一种减少条件判断数量的方法是将if-else组织成一系列嵌套的if-else表达式。使用一个单独的一长串的if-else通常导致运行缓慢,因为每个条件体都要被计算。

查表法
有些情况下要避免使用if-else或switch。当有大量离散值需要测试时,if-else和switch都比使用查表法要慢得多。
在JavaScript中,查表法可使用数组或普通对象实现

1
2
var results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10]
return results[value];

使用查表法时,必须完全消除所有条件判断。操作转换成一个数组项查询或者一个对象成员查询。
使用查表法的一个优点是:由于没有条件判断,当候选值数量增加时,很少,甚至没有增加额外的性能开销。

递归
复杂算法通常比较容易使用递归实现。但递归也存在些问题:一个错误定义,或者缺少终结条件可导致长时间运行,冻结用户界面。此外,递归函数还会遇到浏览器调用栈大小的限制。

调用栈限制
JavaScript引擎所支持的递归数量与JavaScript调用栈大小直接相关。只有IE例外,它的调用栈与可用系统内存相关,其他浏览器有固定的调用栈限制。Chrome 是唯一不显示调用栈溢出错误的浏览器。
关于调用栈溢出错误,最令人感兴趣的部分大概是:在某些浏览器中,他们的确是 JavaScript 错误,可以用一个 try-catch 表达式捕获。异常类型因浏览器而不同。在 Firefox 中,它是一个 InternalError;在 Safari和 Chrome 中,它是一个 RangeError;在 Internet Explorer 中抛出一个一般性的 Error 类型。 (Opera 不抛出错误;它终止 JavaScript 引擎)。这使得我们能够在 JavaScript 中正确处理这些错误:

1
2
3
4
5
try{
    recurse();
}catch(ex){
    alert("Too much recursion!");
}

递归模式
1.直接递归模式(函数调用自身)

1
2
3
4
function recurse(){
    arguments.callee();
}
recurse();

2.精巧模式
包含两个函数:

1
2
3
4
5
6
7
function first(){
    second();
}
function second(){
    first();
}
first();

常见的栈溢出原因是一个不正确的终止条件,所以定位模式错误的第一步是验证终止条件。如果终止条件正确,那么算法包含了太多层递归,为了能够安全地在浏览器中运行,应该改用迭代,制表,或两者兼而有之。

迭代
任何可以用递归实现的算法都可以用迭代实现。使用优化的循环替代长时间运行的递归函数可以提高性能,因为运行一个循环比反复调用一个函数的开销要低。
合并排序算法是最常用的以递归实现的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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));
}

上述合并排序代码相当简单,但是 mergeSort()函数被调用非常频繁,可能在Firefox上导致栈溢出。
改用迭代实现合并排序算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 work = [];
    for(var i = 0,len = items.length; i < len; i++){
        work.push([items[i]]);
    }
    work.push([]);
    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] = [];
    }
    return work[0];
}

此mergeSort()函数采用迭代而不是递归,虽然迭代版本的合并排序可能比递归版本慢些,但它不会像递归版本那样影响调用栈。将递归算法切换为迭代是避免栈溢出错误的方法之一。

制表
减少工作量就是最好的性能优化技术。
制表,通过缓存先前计算结果为后续计算所重复使用,避免了重复工作。因此,常常结合制表来实现递归算法。

1
2
3
4
5
6
7
8
9
10
11
12
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];
}

上述代码使用了制表技术的阶乘函数的关键是建立一个缓存对象。此对象位于函数内部,并预置了两个最简单的阶乘:
0和1。在计算阶乘之前,首先检查缓存中是否已经存在相应的计算结果。没有对应的缓冲值说明这是第一次进行此数值的计算,计算完成之后结果被存入缓存之中,以备今后使用。

为了使一个函数的制表过程更加容易,可定义一个memoize()函数封装基本功能:

1
2
3
4
5
6
7
8
9
10
function memoize(fundamental,cache){
cache = cache || {};
var shell = function(arg){
if(!cache.hasOwnProperty(arg)){
cache[arg] = fundamental(arg);
}
return cache[arg];
};
return shell;
}

此memoize()函数接收两个参数:一个用来制表的函数和一个可选的缓存对象。
调用如下:

1
var memfactorial = memoize(factoroal, {"0": 1,"1":1});

总结
与其它语言不同的是,JavaScript可用资源有限。
1、for,while,do-while循环的性能特性相似,谁也不比谁更快或更慢;
2、除非你要迭代遍历一个属性未知的对象,否则不要使用for-in循环;
3、改善循环性能的最好办法是减少每次迭代中的运算量,并减少循环迭代次数;
4、一般来说,switch总是比if-else更快,但并不总是最好的解决方法;
5、当判断条件较多时,查表法比if-else或者switch更快;
6、浏览器的调用栈尺寸限制了递归算法在JavaScript中的应用:栈溢出错误导致其他代码也不能正常执行;
7、如果遇到一个栈溢出错误,将方法修改为一个迭代算法或者使用制表法可以避免重复工作;

第五章 字符串和正则表达式

字符串拼接
字符串拼接表现出惊人的性能紧张。

字符串拼接的方法:

“+”和“+=”操作符;
数组的join()方法;
字符串的concat()方法。

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

此代码执行时,发生四个步骤:

  • 内存中创建了一个临时字符串
  • 临时字符串的值被赋予“onetwo”
  • 临时字符串与str的值进行连接
  • 结果赋予str

下面的代码通过两个离散表达式直接将内容附加在str上,避免了临时字符串。在大多数浏览器上这样做可加快10%-40%。

1
2
str += "one";
str += "two";

或者

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

使用这种方法进行拼接字符串,必须把str放在第一个拼接的字符串。

除IE以外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字符串拷贝到它的尾部。
如果一个循环中,基本字符串位于最左端,就可以避免多次复制一个越来越大的基本字符串。

在 IE8 中,连接字符串只是记录下构成新字符串的各部分字符串的引用。在最后时刻(当你真正使用连接后的字符串时),各部分字符串才被逐个拷贝到一个新的“真正的”字符串中,然后用它取代先前的字符串引用,所以并非每次使用字符串时都发生合并操作。

IE7 和更早的浏览器在连接字符串时使用更糟糕的实现方法,每连接一对字符串都要把它们复制到一块
新分配的内存中。

Firefox和编译期合并
在赋值表达式中所有字符串连接都属于编译期常量,Firefox自动地在编译过程中合并它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foldingDemo(){
    var str = "compile" + "time" + "folding";
    str += "this" + "works" + "too";
    str = str + "but" + "not" + "this";
}
alert(foldingDemo.toString());
// 输出:
/*
function foldingDemo(){
    var str = "compile" + "time" + "folding";
    str += "this" + "works" + "too";
    str = str + "but" + "not" + "this";
}
*/

当字符串是这样合并在一起时,由于运行时没有中间字符串,所有连接它们的时间和内存可以减少到零。
这种功能非常了不起,但它并不经常起作用,因为通常从运行期数据创建字符串而不是编译期常量。

数组联结(在IE7以及更早的浏览器中,效果显著)
Array.prototype.join方法将数组的所有元素合并成一个字符串,并在每个元素之间插入一个分隔符字符串。
在大多数浏览器上,数组联结比连接字符串的其他方法更慢,但事实上,为一种补偿方法,在IE7和更早的浏览器上,
它是连接大量字符串唯一高效的途径。

String.prototype.concat
原生字符串连接函数接受任意数目的参数,并将每一个参数都追加在调用函数的字符串上。可以追加任意个字符串,或者一个完整的字符串数组。
但在大多数情况下concat比简单的”+”和”+=”慢一些,而且在IE,Opera和Chrome上大幅变慢。

正则表达式优化
粗浅地编写正则表达式是造成性能瓶颈的主要原因。

第六章 响应接口

确保网页应用程序的响应速度也是一个重要的性能关注点。

大多数浏览器有一个单独的处理进程,它由两个任务所共享:JavaScript任务和用户界面更新任务。
每个时刻只有其中一个操作得以执行,即当JavaScript运行时,用户界面就被”锁定“了。

浏览器UI线程
JavaScript和UI更新共享的进程通常被称作浏览器UI线程。此UI线程围绕一个简单的队列系统工作,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。
此进程中最令人感兴趣的部分是每次输入均导致一个或多个任务被加入队列。
按下一个按钮,然后屏幕上显示出一个消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>

线程触发的过程
线程触发

浏览器限制
浏览器在JavaScript运行时间上采取了限制。这是一个有必要的限制,确保恶意代码编写者不能通过无尽的密集操作
锁定用户浏览器或计算机。
此类限制有两个:调用栈尺寸限制和长时间脚本限制。

长运行脚本限制有时被称作长运行脚本定时器或者失控脚本定时器,但其基本思想是浏览器记录一个脚本的运行时间,一旦到达一定限度时就终止它。

有两种测量脚本运行时间的方法:
1)统计自脚本开始运行以来执行过多少语句;
2)统计脚本运行的总时间。

长运行脚本最好的处理办法首先是避免他们。

多久才算”太久“
一个单一的JavaScript操作应当使用的总时间(最大)是100毫秒。
如果某个接口在100毫秒内响应用户输入,用户认为自己是”直接操作用户界面的对象“,超过100毫秒意味着用户认为自己与接口断开了。

用定时器让出时间片
在某些情况下,还是有一些JavaScript任务因为复杂性原因不能再100毫秒或更少时间内完成。这种情况下,理想办法是让出对UI线程的控制,使UI更新可以进行。

定时器基础
在JavaScript中使用setTimeout()或setInterval()创建定时器。
setTimeout()函数创建一个定时器只运行一次,而setInterval()函数创建一个周期性重复运行的定时器。

定时器与UI线程交互的方式有助于分解长运行脚本成为较短的片断。调用setTimeout()或setInterval()告诉JavaScript引擎等待一定时间,然后将JavaScript任务添加到UI队列中。

1
2
3
4
5
function greeting(){
    alert("Hello world!");
}
setTimeout(greeting, 250);

第二个参数”250“指出的是什么时候应当将任务添加到UI队列,并不是说那时代码就将被执行,这个任务必须等到队列中的其他任务都执行之后才能被执行。

定时器代码只有等创建它的函数运行完成之后,才有可能被执行。
在任何一种情况下,创建一个定时器造成UI线程暂停,如同它从一个任务切换到下一个任务。因此,定时器代码复位所有相关的浏览器限制,包括长运行脚本时间。此外调用栈也在定时器代码中复位为零。

如果调用 setTimeout()的函数又调用了其他任务,耗时超过定时器延时,定时器代码将立即被执行,它与主调函数之间没有可察觉的延迟。
最小定时器延时应设置25毫秒。

在数组处理中使用定时器
一个常见的长运行脚本就是循环占用了太长的运行时间。经过循环优化后,还是不能缩减足够的运行时间,那么定时器还能更进一步地优化。基本方法是:将循环工作分解到定时器序列中。

1
2
3
for(var i = 0,len = items.length; i < len; i++){
    process(items[i]);
}

上述循环结构运行时间过长的原因有二:process()的复杂度,items的大小。

判断是否可用定时器取代循环的两个决定性因素?
1)此处理过程必须是同步处理吗?
2)数据必须按顺序处理吗?

如果两个回答都是“否”,那么代码将适于使用定时器分解工作。
一种基本异步代码模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 = [123789323778232654219543321160];
function outputValue(value){
    console.log(value);
}
processArray(items, outputValue, function(){
    console.log("Done!");
});

上述代码的基本思想是:

创建一个原始数组的克隆,将它作为处理对象。第一次调用setTimeout()创建一个定时器处理队列中的第一个项。
调用todo.shift()返回它的第一个项然后将它从数组中删除。此值作为参数传给process()。
然后,检查是否还有更多项需要处理。如果todo队列中还有内容,那么再启动一个定时器。如果不再有内容需要处理,将调用callback()函数。

分解任务
我们通常将一个任务分解成一系列子任务。可将一行代码简单地看作一个原子任务,多行代码组合在一起构成一个独立任务。如果函数运行时间太长,它可以拆分成一系列更小的步骤,把独立方法放在定时器中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function saveDocument(id){
    openDocument(id)
    writeText(id);
    closeDocument(id);
    updateUI(id);
}
function saveDocument(id){
    var tasks = [openDocument, writeText, closeDocument, updateUI];
    setTimeout(function(){
        var task = tasks.shift();
        task(id);
        if (tasks.length > 0){
            setTimeout(arguments.callee, 25);
        }
    }, 25);
}

不要让任何JavaScript代码持续运行超过50毫秒,确保代码永远不会影响用户体验。
通过原生的Date对象跟踪代码的运行时间。这是大多数JavaScript分析工具所采用的工作方式:

1
2
3
4
5
6
7
8
9
var start = +new Date(),    //加号“+”将Date对象转换为一个数字
      stop;
someLongProcess();
stop = +new Date();
if(stop - start <50){
    alert("something");
}else{
    alert("taking too long.");
}

有时每次只执行一个任务效率不高。在定时器循环中加入时间检测机制,使得每次处理多个数据,这样避免了将任务分解成过于碎小的片断。

定时器和性能
定时器使得JavaScript代码整体性能表现出巨大差异,但过度使用它们会对性能产生负面影响。
使用定时器序列时,同一时间只有一个定时器存在,只有当这个定时器结束才创建一个新的定时器。
此外,还应该在web应用中限制高频率重复定时器的数量。建议创建一个单独的重复定时器,每次执行多个操作。

Web Workers(网页工人线程)
自JavaScript诞生以来,还没有办法在浏览器UI线程之外运行代码。Web workers API 改变了这种状况,它引入一个接口,使代码运行而不占用浏览器UI线程的时间。
web workers在自己的线程中运行JavaScript,意味着,web workers中的代码运行不仅不会影响浏览器UI,而且也不会影响其他web workers中运行的代码。

工人线程运行环境
由于web workers不绑定UI线程,这也意味着它们将不能访问更多的浏览器资源。Web workers 修改DOM将导致用户界面出错,但每个web worker都有自己的全局运行环境,只有JavaScript特性的一个子集可用。

工人线程的运行环境由下列部分组成:

一个浏览器对象,只包含四个属性:appName,appVersion,userAgent和platform;
一个location对象(和window对象一样,只是所有属性都是只读的);
一个self对象指向全局工人线程对象;
一个importScript()方法,使得工人线程可以加载外部JavaScript文件;
所有ECMAScript对象,诸如Object,Array,Date等等;
XMLHTTPRequest构造器;
setTimeout()和setInterval()方法;
一个close()方法,可以立即停止工人线程。

因为web worker有不同的全局运行环境,你不能在JavaScript代码中创建。需要创建一个完全独立的JavaScript文件,包含那些在工人线程中运行的代码。要创建web workers,必须要传入这个JavaScript文件的URL:

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

此代码一旦执行,将为指定文件创建一个新线程和一个新的工人线程运行环境。此文件被异步下载,直到下载并运行完之后才启动工人线程。

工人线程交互
工人线程和网页代码通过事件接口进行交互。网页代码可通过postMessage()方法向工人线程传递数据,它接收单个参数,即传递给工人线程的数据。此外,在工人线程中还有onmessage事件句柄用于接收信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//WEB页主线程
var worker = new Worker("worker.js"); 
//创建一个Worker对象并向它传递将在新线程中执行的脚本的URL
worker.postMessage("hello world");     
//向worker发送数据
worker.onmessage =function(evt){     
//接收worker传过来的数据函数
console.log(evt.data);         
    //输出worker发送来的数据
}
//worker.js
onmessage =function (evt){
var d = evt.data;
//通过evt.data获得发送来的数据
postMessage( d );
//将获取到的数据发送会主线程
}

消息系统是页面和工人线程之间唯一的交互途径。
只有某些类型的数据可以使用postMessage()传递。可以传递的类型有:string,number,boolean,null,undefined以及Objec和Array的实例。

加载外部文件
当工人线程用过importScripts()方法加载外部JavaScript文件,它接收一个或多个URL参数,指出要加载的JavaScript文件网址。
工人线程以阻塞方式调用importScripts(),直到所有文件加载完成并执行之后,脚本才继续运行。但由于工人线程在UI线程之外运行,这种阻塞不会影响UI响应。

Web Workers的实际用途
Web Workers适合于那些纯数据的,或者与浏览器UI没关系的长运行脚本。
例如:解析一个很大的 JSON 字符串(JSON 解析将在后面第七章讨论)。假设数据足够
大,至少需要 500 毫秒才能完成解析任务。很显然时间太长了以至于不能允许 JavaScript 在客户端上运行
它,因为它会干扰用户体验。此任务难以分解成用于定时器的小段任务,所以工人线程成为理想的解决方
案。

1
2
3
4
5
6
7
8
9
10
11
12
13
var worker = new Worker("jsonparser.js");
worker.onmessage = function(event){
    var jsonData = event.data;
    evaluateData(jsonData);
};
worker.postMessage(jsonText);
//jsonparser.js
self.onmessage = function(event){
    var jsonText = event.data;
    var jsonData = JSON.parse(jsonText);
    self.postMessage(jsonData);
};

此工程只能在Firefox 3.5和更高版本中运行,而Safari 4 和Chrome 3中,页面和工人线程之间只允许传递字符串。

解析一个大字符串只是许多受益于web workers的任务之一。其他可能受益的任务如下:

编/解码一个大字符串;
复杂数学运算(包括图像或视频处理);
给一个大数组排序。

任何超过100毫秒的处理,都应当考虑工人线程方案是不是比基于定时器的方案更合适。当然,还要基于浏览器是否支持工人线程。

总结
JavaScript和用户界面更新在同一个进程内运行,同一时刻只有其中一个可以运行。有效管理UI线程就是要确保JavaScript不能运行太长时间,以免影响用户体验。牢记以下几点:

JavaScript运行时间不应该超过100毫秒;
JavaScript运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript长时间运行将导致用户体验混乱和脱节;
定时器可用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务;
Web Workers是新式浏览器才支持的特性,它允许你在UI线程之外运行JavaScript代码而避免锁定UI。

坚持原创技术分享,您的支持将鼓励我继续创作!