本文为 《高性能 JavaScript》 读书笔记,是利用中午休息时间、下班时间以及周末整理出来的,此书虽有点老旧,但谈论的性能优化话题是每位同窗必须理解和掌握的,业务响应速度直接影响用户体验。javascript
大多数浏览器使用单进程处理UI
更新和JavaScript
运行等多个任务,而同一时间只能有一个任务被执行
将全部script
标签放在页面底部,紧靠</body>
上方,以保证页面脚本运行以前完成解析css
<html> <head> </head> <body> <p>Hello World</p> <!-- --> <script type="text/javascript" src="file.js"></script> </body> </html>
常规script
脚本浏览器会当即加载并执行,异步加载使用async
与defer
两者区别在于aysnc
为无序,defer
会异步根据脚本位置前后依次加载执行html
<!-- file一、file2依次加载 --> <script type="text/javascript" src="file1.js" defer></script> <script type="text/javascript" src="file2.js" defer></script>
<!-- file一、file2无序加载 --> <script type="text/javascript" src="file1.js" async></script> <script type="text/javascript" src="file2.js" async></script>
不管在何处启动下载,文件的下载和运行都不会阻塞其余页面处理过程。你甚至能够将这些代码放在<head>
部分而不会对其他部分的页面代码形成影响(除了用于下载文件的 HTTP
链接)前端
var script = document.createElement("script"); script.type = "text/javascript"; script.src = "file1.js"; document.getElementsByTagName("head")[0].appendChild(script);
function loadScript(url, callback) { var script = document.createElement("script"); script.type = "text/javascript"; if (script.readyState) { //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function() { callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
前提条件为同域,此处与异步加载同样,只不过使用的是 XMLHttpRequestjava
script
标签放在页面底部,紧靠 body 关闭标签上方,以保证页面脚本运行以前完成解析数据存储在哪里,关系到代码运行期间数据被检索到的速度.每一种数据存储位置都具备特定的读写操做负担。大多数状况下,对一个直接量和一个局部变量数据访问的性能差别是微不足道的。
对 DOM 操做代价昂贵,在富网页应用中一般是一个性能瓶颈。一般处理如下三点ajax
经过 DOM 事件处理用户响应正则表达式
一个很形象的比喻是把 DOM 当作一个岛屿,把 JavaScript(ECMAScript)当作另外一个岛屿,二者之间以一座收费桥链接(参见 John Hrvatin,微软,MIX09, http://videos.visitmix.com/MI...)。每次 ECMAScript 须要访问 DOM 时,你须要过桥,交一次“过桥费”。你操做 DOM 次数越多,费用就越高。通常的建议是尽可能减小过桥次数,努力停留在 ECMAScript 岛上。
访问或修改元素最坏的状况是使用循环执行此操做,特别是在 HTML 集合中使用循环算法
function innerHTMLLoop() { for (var count = 0; count < 15000; count++) { document.getElementById("here").innerHTML += "a"; } }
此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对 DOM 元素访问两次:一次
读取 innerHTML 属性能容,另外一次写入它shell
优化以下编程
function innerHTMLLoop2() { var content = ""; for (var count = 0; count < 15000; count++) { content += "a"; } document.getElementById("here").innerHTML += content; }
你访问 DOM 越多,代码的执行速度就越慢。所以,通常经验法则是:轻轻地触摸 DOM,并尽可能保持在 ECMAScript 范围内
使用 DOM 方法更新页面内容的另外一个途径是克隆已有 DOM 元素,而不是建立新的——即便用 element.cloneNode()(element 是一个已存在的节点)代替 document.createElement();
代码总体结构是执行速度的决定因素之一。代码量少不必定执行快,代码量多,也不必定执行慢,性能损失与代码组织方式和具体问题解决办法直接相关。
在大多数编程语言中,代码执行时间多数在循环中度过。在一系列编程模式中,循环是最多见的模式之一,提升性能必须控制好循环,死循环和长时间循环会严重影响用户体验。
前三种循环几乎全部编程语言都能通用,for in 循环遍历对象命名属性(包括自有属性和原型属性)
循环性能争论的源头是应当选用哪一种循环,在 JS 中 for-in 比其余循环明显要慢(每次迭代都要搜索实例或原型属性),除非对数目不详的对象属性进行操做,不然避免使用 for-in。除开 for-in,选择循环应当基于需求而不是性能
减小每次迭代的操做总数能够大幅提升循环的总体性能
优化循环:
编程中常常会听到此说法,如今来验证一下,测试样例
var arr = []; for (var i = 0; i < 100000000; i++) { arr[i] = i; } var start = +new Date(); for (var j = arr.length; j > -1; j--) { arr[j] = j; } console.log("倒序循环耗时:%s ms", Date.now() - start); //约180 ms var start = +new Date(); for (var j = 0; j < arr.length; j++) { arr[j] = j; } console.log("正序序循环耗时:%s ms", Date.now() - start); //约788 ms
尽管基于函数的迭代显得更加便利,它仍是比基于循环的迭代要慢一些。每一个数组项要关联额外的函数调用是形成速度慢的缘由。在全部状况下,基于函数的迭代占用时间是基于循环的迭代的八倍,所以在关注执行时间的状况下它并非一个合适的办法。
使用 if-else 或者 switch 的流行理论是基于测试条件的数量:条件数量较大,倾向使用 switch,更易于阅读
当条件体增长时,if-else 性能负担增长的程度比 switch 更多。
通常来讲,if-else 适用于判断两个离散的值或者几个不一样的值域,若是判断条件较多 switch 表达式将是更理想的选择
会受浏览器调用栈大小的限制
任何能够用递归实现的算法能够用迭代实现。使用优化的循环替代长时间运行的递归函数能够提升性能,由于运行一个循环比反复调用一个函数的开销要低
斐波那契
function fibonacci(n) { if (n === 1) return 1; if (n === 2) return 2; return fibonacci(n - 1) + fibonacci(n - 2); }
//制表 function memorize(fundamental, cache) { cache = cache || {}; var shell = function(args) { if (!cache.hasOwnProperty(args)) { cache[args] = fundamental(args); } return cache[args]; }; return shell; } //动态规划 function fibonacciOptimize(n) { if (n === 1) return 1; if (n === 2) return 2; var current = 2; var previous = 1; for (var i = 3; i <= n; i++) { var temp = current; current = previous + current; previous = temp; } return current; } //计算阶乘 var res1 = fibonacci(40); var res2 = memorize(fibonacci)(40); var res3 = fibonacciOptimize(40); //计算出来的res3优于res2,res2优于res1
运行代码的总量越大,优化带来的性能提高越明显
正如其余编程语言,代码的写法与算法选用影响 JS 的运行时间,与其余编程语言不一样,JS 可用资源有限,因此优化当然重要
在 JS 中,正则是必不可少的东西,它的重要性远远超过烦琐的字符串处理
字符串链接表现出惊人的性能紧张。一般一个任务经过一个循环,向字符串末尾不断地添加内容,来建立一个字符串(例如,建立一个 HTML 表或者一个 XML 文档),但此类处理在一些浏览器上表现糟糕而遭人痛恨
Method | Example |
---|---|
+ | str = 'a' + 'b' + 'c'; |
+= | str = 'a'; str += 'b'; str += 'c'; |
array.join() | str = ['a','b','c'].join(''); |
string.concat() | str = 'a'; str = str.concat('b', 'c'); |
当链接少许的字符串,上述的方式都很快,可根据本身的习惯使用;
当合并字符串的长度和数量增长以后,有些函数就开始发挥其做用了
str += "a" + "b";
此代码执行时,发生四个步骤
下面的代码经过两个离散的表达式直接将内容附加在 str 上避免了临时字符串
str += "a"; str += "b";
事实上用一行代码就能够解决
str = str + "a" + "b";
赋值表达式以 str 开头,一次追加一个字符串,从左至右依次链接。若是改变了链接顺序(例如:str = 'a' + str + 'b'
),你会失去这种优化,这与浏览器合并字符串时分配内存的方法有关。除 IE 外,浏览器尝试扩展表达式左端字符串的内存,而后简单地将第二个字符串拷贝到它的尾部。若是在一个循环中,基本字符串在左端,能够避免屡次复制一个愈来愈大的基本字符串。
Array.prototype.join 将数组的全部元素合并成一个字符串,并在每一个元素之间插入一个分隔符字符串。若传递一个空字符串,可将数组的全部元素简单的拼接起来
var start = Date.now(); var str = "I'm a thirty-five character string.", newStr = "", appends = 5000000; while (appends--) { newStr += str; } var time = Date.now() - start; console.log("耗时:" + time + "ms"); //耗时:1360ms
var start = Date.now(); var str = "I'm a thirty-five character string.", strs = [], newStr = "", appends = 5000000; while (appends--) { strs[strs.length] = str; } newStr = strs.join(""); var time = Date.now() - start; console.log("耗时:" + time + "ms"); //耗时:414ms
这一难以置信的改进结果是由于避免了重复的内存分配和拷贝愈来愈大的字符串。
原生字符串链接函数接受任意数目的参数,并将每个参数都追加在调用函数的字符串上
var str = str.concat(s1); var str = str.concat(s1, s2, s3); var str = String.prototype.concat.apply(str, array);
大多数状况下 concat 比简单的+或+=慢一些
许多因素影响正则表达式的效率,首先,正则适配的文本千差万别,部分匹配时比彻底不匹配所用的时间要长,每种浏览器的正则引擎也有不一样的内部优化
在大多数现代正则表达式实现中(包括 JavaScript 所需的),回溯是匹配过程的基本组成部分。它很大程度上也是正则表达式如此美好和强大的根源。然而,回溯计算代价昂贵,若是你不够当心的话容易失控。虽然回溯是总体性能的惟一因素,理解它的工做原理,以及如何减小使用频率,多是编写高效正则表达式最重要的关键点。
正则表达式匹配过程
- 当一个正则表达式扫描目标字符串时,它从左到右逐个扫描正则表达式的组成部分,在每一个位置上测试能不能找到一个匹配。对于每个量词和分支,都必须决定如何继续进行。若是是一个量词(诸如*,+?,或者{2,}),正则表达式必须决定什么时候尝试匹配更多的字符;若是遇到分支(经过|操做符),它必须从这些选项中选择一个进行尝试。
- 每当正则表达式作出这样的决定,若是有必要的话,它会记住另外一个选项,以备未来返回后使用。若是所选方案匹配成功,正则表达式将继续扫描正则表达式模板,若是其他部分匹配也成功了,那么匹配就结束了。可是若是所选择的方案未能发现相应匹配,或者后来的匹配也失败了,正则表达式将回溯到最后一个决策点,而后在剩余的选项中选择一个。它继续这样下去,直到找到一个匹配,或者量词和分支选项的全部可能的排列组合都尝试失败了,那么它将放弃这一过程,而后移动到此过程开始位置的下一个字符上,重复此过程。
示例分析
/h(ello|appy) hippo/.test("hello there, happy hippo");
此正则表达式匹配“hello hippo”或“happy hippo”。测试一开始,它要查找一个 h,目标字符串的第一个字母刚好就是 h,它马上就被找到了。接下来,子表达式(ello|appy)提供了两个处理选项。正则表达式选择最左边的选项(分支选择老是从左到右进行),检查 ello 是否匹配字符串的下一个字符。确实匹配,而后正则表达式又匹配了后面的空格。然而在这一点上它走进了死胡同,由于 hippo 中的 h 不能匹配字符串中的下一个字母 t。此时正则表达式还不能放弃,由于它尚未尝试过全部的选择,随后它回溯到最后一个检查点(在它匹配了首字母 h 以后的那个位置上)并尝试匹配第二个分支选项。可是没有成功,并且也没有更多的选项了,因此正则表达式认为从字符串的第一个字符开始匹配是不能成功的,所以它从第二个字符开始,从新进行查找。它没有找到 h,因此就继续向后找,直到第 14 个字母才找到,它匹配 happy 的那个 h。而后它再次进入分支过程。此次 ello 未能匹配,可是回溯以后第二次分支过程当中,它匹配了整个字符串“happy hippo”(如图 5-4)。匹配成功了。
当一个正则表达式占用浏览器上秒,上分钟或者更长时间时,问题缘由极可能是回溯失控。正则表达式处理慢每每是由于匹配失败过程慢,而不是匹配成功过程慢。
var reg = /<html>[\s\S]*?<head>[\s\S]*?<title>[\s\S]*?<\/title>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/; //优化以下 var regOptimize = /<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<title>))\2(?=([\s\S]*?<\/title>))\3(?=([\s\S]*?<\/head>))\4(?=([\s\S]*?<body>))\5(?=([\s\S]*?<\/body>))\6[\s\S]*?<\/html>/;
如今若是没有尾随的</html>那么最后一个[sS]*?将扩展至字符串结束,正则表达式将马上失败由于没有回溯点能够返回
var endsWithSemicolon = /;$/.test(str);
你可能以为很奇怪,虽然说当前没有哪一个浏览器聪明到这个程度,可以意识到这个正则表达式只能匹配字符串的末尾。最终它们所作的将是一个一个地测试了整个字符串。字符串的长度越长(包含的分号越多),它占用的时间也越长
var endsWithSemicolon = str.charAt(str.length - 1) == ";";
这种状况下,更好的办法是跳过正则表达式所需的全部中间步骤,简单地检查最后一个字符是否是分号:
这个例子使用 charAt 函数在特定位置上读取字符。字符串函数 slice,substr,和 substring 可用于在特定位置上提取并检查字符串的值
全部这些字符串操做函数速度都很快,当您搜索那些不依赖正则表达式复杂特性的文本字符串时,它们有助于您避免正则表达式带来的性能开销
正则表达式容许你用不多的代码实现一个修剪函数,这对 JavaScript 关心文件大小的库来讲十分重要。可能最好的全面解决方案是使用两个子表达式:一个用于去除头部空格,另外一个用于去除尾部空格。这样处理简单而迅速,特别是处理长字符串时。
//方法 用正则表达式修剪 // trim1 String.prototype.trim = function() { return this.replace(/^\s+/, "").replace(/\s+$/, ""); }; //trim2 String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); }; // trim 3 String.prototype.trim = function() { return this.replace(/^\s*([\s\S]*?)\s*$/, "$1"); }; // trim 4 String.prototype.trim = function() { return this.replace(/^\s*([\s\S]*\S)?\s*$/, "$1"); }; // trim 5 String.prototype.trim = function() { return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1"); }; //方法二 不使用正则表达式修剪 String.prototype.trim = function() { var start = 0; var end = this.length - 1; //ws 变量包括 ECMAScript 5 中定义的全部空白字符 var ws = "\n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\ufeff"; while (ws.indexOf(this.charAt(start)) > -1) { start++; } while (end > start && ws.indexOf(this.charAt(end)) > -1) { end--; } return this.slice(start, end + 1); }; //方法三 混合解决方案 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); };
简单地使用两个子正则表达式在全部浏览器上处理不一样内容和长度的字符串时,均表现出稳定的性能。所以它能够说是最全面的解决方案。混合解决方案在处理长字符串时特别快,其代价是代码稍长,在某些浏览器上处理尾部长空格时存在弱点
用户倾向于重复尝试这些不发生明显变化的动做,因此确保网页应用程序的响应速度也是一个重要的性能关注点
JavaScript 和 UI 更新共享的进程一般被称做浏览器 UI 线程, UI 线程围绕着一个简单的队列系统工做,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。这些任务不是运行 JavaScript 代码,就是执行 UI 更新,包括重绘和重排版.
大多数浏览器在 JavaScript 运行时中止 UI 线程队列中的任务,也就是说 JavaScript 任务必须尽快结束,以避免对用户体验形成不良影响
Brendan Eich,JavaScript 的创造者,引用他的话说,“[JavaScript]运行了整整几秒钟极可能是作错了什么……”
定时器与 UI 线程交互的方式有助于分解长运行脚本成为较短的片段
全部浏览器试图尽量准确,但一般会发生几毫秒滑移,或快或慢。正由于这个缘由,定时器不可用于测量实际时间
目前最经常使用的方法中,XMLHttpRequest(XHR)用来异步收发数据。全部现代浏览器都可以很好地支持它,并且可以精细地控制发送请求和数据接收。你能够向请求报文中添加任意的头信息和参数(包括 GET 和 POST),并读取从服务器返回的头信息,以及响应文本自身
五种经常使用技术用于向服务器请求数据
//封装ajax var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status >= 200) { // } }; xhr.open(type, url, true); xhr.setRequestHeader("Content-Type", contentType); xhr.send(null);
经过 Douglas Crockford 的发明与推广,JSON 是一个轻量级并易于解析的数据格式,它按照 JavaScript 对象和数组字面语法所编写
数据传输技术和数据格式
高性能 Ajax 包括:知道你项目的具体需求,选择正确的数据格式和与之相配的传输技术
封装本身的 ajax 库
(function(root) { root.MyAjax = (config = {}) => { let url = config.url; let type = config.type || "GET"; let async = config.async || true; let headers = config.headers || []; let contentType = config.contentType || "application/json;charset=utf-8"; let data = config.data; let dataType = config.dataType || "json"; let successFn = config.success; let errorFn = config.error; let completeFn = config.complete; let xhr; if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { let rsp = xhr.responseText || xhr.responseXML; if (dataType === "json") { rsp = eval("(" + rsp + ")"); } successFn(rsp, xhr.statusText, xhr); } else { errorFn(xhr.statusText, xhr); } if (completeFn) { completeFn(xhr.statusText, xhr); } } }; xhr.open(type, url, async); //设置超时 if (async) { xhr.timeout = config.timeout || 0; } //设置请求头 for (let i = 0; i < headers.length; ++i) { xhr.setRequestHeader(headers[i].name, headers[i].value); } xhr.setRequestHeader("Content-Type", contentType); //send if ( typeof data == "object" && contentType === "application/x-www-form-urlencoded" ) { let s = ""; for (attr in data) { s += attr + "=" + data[attr] + "&"; } if (s) { s = s.slice(0, s.length - 1); } xhr.send(s); } else { xhr.send(data); } }; })(window);
位操做运算符
四种位逻辑操做符
num % 2 === 0; //取模与0进行判断 num & 1; //位与1结果位1则为奇数,为0则为偶数
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 }
像这样的位掩码操做很是快,正由于前面提到的缘由,操做发生在系统底层。若是许多选项保存在一块儿并常常检查,位掩码有助于加快总体性能
不管你怎样优化 JavaScript 代码,它永远不会比 JavaScript 引擎提供的原生方法更快。经验不足的 JavaScript 开发者常常犯的一个错误是在代码中进行复杂的数学运算,而没有使用内置 Math 对象中那些性能更好的版本。Math 对象包含专门设计的属性和方法,使数学运算更容易。
//查看Math对象全部方法 Object.getOwnPropertyNames(Math);
当网页或应用程序变慢时,分析网上传来的资源,分析脚本的运行性能,使你可以集中精力在那些须要努力优化的地方。
能读到最后的同窗也不容易,毕竟篇幅稍长。本书大概花了三周的零碎时间读完,建议你们读一读。若是你们在看书过程当中存在疑问,不妨打开电脑验证书中做者的言论,或许会更加深入。
若文中有错误欢迎你们评论指出,或者加我微信好友一块儿交流
gm4118679254