12. 使用事件委托
事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术, 使用事件委托可以节省内存。
<ul> <li>苹果</li> <li>香蕉</li> <li>凤梨</li> </ul> // good document.querySelector('ul').onclick = (event) => { const target = event.target if (target.nodeName === 'LI') { console.log(target.innerHTML) } } // bad document.querySelectorAll('li').forEach((e) => { e.onclick = function() { console.log(this.innerHTML) } })
13. 注意程序的局部性
一个编写良好的计算机程序常常具有良好的局部性,它们倾向于引用最近引用过的数据项附近的数据项,或者最近引用过的数据项本身,这种倾向性,被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。
局部性通常有两种不同的形式:
- 时间局部性:在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来被多次引用。
- 空间局部性 :在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。
时间局部性示例
function sum(arry) { let i, sum = 0 let len = arry.length for (i = 0; i < len; i++) { sum += arry[i] } return sum }
在这个例子中,变量sum在每次循环迭代中被引用一次,因此,对于sum来说,具有良好的时间局部性
空间局部性示例
具有良好空间局部性的程序
// 二维数组 function sum1(arry, rows, cols) { let i, j, sum = 0 for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { sum += arry[i][j] } } return sum }
空间局部性差的程序
// 二维数组 function sum2(arry, rows, cols) { let i, j, sum = 0 for (j = 0; j < cols; j++) { for (i = 0; i < rows; i++) { sum += arry[i][j] } } return sum }
看一下上面的两个空间局部性示例,像示例中从每行开始按顺序访问数组每个元素的方式,称为具有步长为1的引用模式。
如果在数组中,每隔k个元素进行访问,就称为步长为k的引用模式。
一般而言,随着步长的增加,空间局部性下降。
这两个例子有什么区别?区别在于第一个示例是按行扫描数组,每扫描完一行再去扫下一行;第二个示例是按列来扫描数组,扫完一行中的一个元素,马上就去扫下一行中的同一列元素。
数组在内存中是按照行顺序来存放的,结果就是逐行扫描数组的示例得到了步长为 1 引用模式,具有良好的空间局部性;而另一个示例步长为 rows,空间局部性极差。
性能测试
运行环境:
- cpu: i5-7400
- 浏览器: chrome 70.0.3538.110
对一个长度为9000的二维数组(子数组长度也为9000)进行10次空间局部性测试,时间(毫秒)取平均值,结果如下:
所用示例为上述两个空间局部性示例
步长为 1 | 步长为 9000 |
124 | 2316 |
从以上测试结果来看,步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。
总结:
- 重复引用相同变量的程序具有良好的时间局部性
- 对于具有步长为 k 的引用模式的程序,步长越小,空间局部性越好;而在内存中以大步长跳来跳去的程序空间局部性会很差
参考资料:
14. if-else 对比 switch
当判断条件数量越来越多时,越倾向于使用 switch 而不是 if-else。
if (color == 'blue') { } else if (color == 'yellow') { } else if (color == 'white') { } else if (color == 'black') { } else if (color == 'green') { } else if (color == 'orange') { } else if (color == 'pink') { } switch (color) { case 'blue': break case 'yellow': break case 'white': break case 'black': break case 'green': break case 'orange': break case 'pink': break }
像上面的这种情况,从可读性来说,使用 switch 是比较好的(js 的 switch 语句不是基于哈希实现,而是循环判断,所以说 if-else、switch 从性能上来说是一样的)。
15. 查找表
当条件语句特别多时,使用 switch 和 if-else 不是最佳的选择,这时不妨试一下查找表。查找表可以使用数组和对象来构建。
switch (index) { case '0': return result0 case '1': return result1 case '2': return result2 case '3': return result3 case '4': return result4 case '5': return result5 case '6': return result6 case '7': return result7 case '8': return result8 case '9': return result9 case '10': return result10 case '11': return result11 }
可以将这个 switch 语句转换为查找表
const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11] return results[index]
如果条件语句不是数值而是字符串,可以用对象来建立查找表
const map = { red: result0, green: result1, } return map[color]
16. 避免页面卡顿
60fps 与设备刷新率
目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。
假如你用 JavaScript 修改了 DOM,并触发样式修改,经历重排重绘最后画到屏幕上。如果这其中任意一项的执行时间过长,都会导致渲染这一帧的时间过长,平均帧率就会下降。假设这一帧花了 50 ms,那么此时的帧率为 1s / 50ms = 20fps,页面看起来就像卡顿了一样。
对于一些长时间运行的 JavaScript,我们可以使用定时器进行切分,延迟执行。
for (let i = 0, len = arry.length; i < len; i++) { process(arry[i]) }
假设上面的循环结构由于 process() 复杂度过高或数组元素太多,甚至两者都有,可以尝试一下切分。
const todo = arry.concat() setTimeout(function() { process(todo.shift()) if (todo.length) { setTimeout(arguments.callee, 25) } else { callback(arry) } }, 25)
如果有兴趣了解更多,可以查看一下高性能JavaScript第 6 章和高效前端:Web高效编程与优化实践第 3 章。
参考资料:
17. 使用 requestAnimationFrame 来实现视觉变化
从第 16 点我们可以知道,大多数设备屏幕刷新率为 60 次/秒,也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候,最好的情况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的唯一方式是使用 requestAnimationFrame
。
/** * If run as a requestAnimationFrame callback, this * will be run at the start of the frame. */ function updateScreen(time) { // Make visual updates here. } requestAnimationFrame(updateScreen);
如果采取 setTimeout
或 setInterval
来实现动画的话,回调函数将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿。
参考资料:
18. 使用 Web Workers
Web Worker 使用其他工作线程从而独立于主线程之外,它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序(反之亦然)。
Web Worker 适用于那些处理纯数据,或者与浏览器 UI 无关的长时间运行脚本。
创建一个新的 worker 很简单,指定一个脚本的 URI 来执行 worker 线程(main.js):
var myWorker = new Worker('worker.js'); // 你可以通过postMessage() 方法和onmessage事件向worker发送消息。 first.onchange = function() { myWorker.postMessage([first.value,second.value]); console.log('Message posted to worker'); } second.onchange = function() { myWorker.postMessage([first.value,second.value]); console.log('Message posted to worker'); }
在 worker 中接收到消息后,我们可以写一个事件处理函数代码作为响应(worker.js):
onmessage = function(e) { console.log('Message received from main script'); var workerResult = 'Result: ' + (e.data[0] * e.data[1]); console.log('Posting message back to main script'); postMessage(workerResult); }
onmessage处理函数在接收到消息后马上执行,代码中消息本身作为事件的data属性进行使用。这里我们简单的对这2个数字作乘法处理并再次使用postMessage()方法,将结果回传给主线程。
回到主线程,我们再次使用onmessage以响应worker回传的消息:
myWorker.onmessage = function(e) { result.textContent = e.data; console.log('Message received from worker'); }
在这里我们获取消息事件的data,并且将它设置为result的textContent,所以用户可以直接看到运算的结果。
不过在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。
参考资料:
19. 使用位操作
JavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。但是在位操作中,数字被转换为有符号的 32 位格式。即使需要转换,位操作也比其他数学运算和布尔操作快得多。
取模
由于偶数的最低位为 0,奇数为 1,所以取模运算可以用位操作来代替。
if (value % 2) { // 奇数 } else { // 偶数 } // 位操作 if (value & 1) { // 奇数 } else { // 偶数 }
取整
~~10.12 // 10 ~~10 // 10 ~~'1.5' // 1 ~~undefined // 0 ~~null // 0
位掩码
const a = 1 const b = 2 const c = 4 const options = a | b | c
通过定义这些选项,可以用按位与操作来判断 a/b/c 是否在 options 中。
// 选项 b 是否在选项中 if (b & options) { ... }