高性能javascript读书笔记(1)

做技术两年,犯的最大的错误就是没有记录遇到的问题和解决方法,过了一段时间就都忘了,没有积累,所以今年开始要多记录一些东西

书《高性能javascript》中英文对照版,看英文太慢,就偷懒了

1。加载和运行

浏览器在遇到<script>标签时,会停下页面下载和解析的过程,运行<script>标签里的代码,如果是外部(src属性)的,浏览器会先下载然后再运行,运行完毕后,才会继续页面的下载和解析;

以前的浏览器js文件会串行下载,下载完一个执行之,然后才下载另一个,现在的浏览器可以并行下载js文件,但是执行还是按照顺序执行。注意这里说的串行并行都是js文件之间的,和图片这些资源之间没关系,js的下载与执行还是会阻塞其他资源的下载的

link会阻塞<script>标签,因为要保证js执行时获取到了准确的样式信息,参考https://blog.csdn.net/hukaihe/article/details/55210424

script标签的url可以是多个文件拼成一个url的,但是似乎不常见?

script标签的defer属性、async属性

动态创建script标签,完成时IE和别的浏览器有不同的完成事件,注意兼容

xhr也可以动态获取js内容,填入script.text中

var script = document.createElement ("script");
script.type = "text/javascript";
script.text = xhr.responseText;//xhr返回
document.body.appendChild(script);

2。数据访问

函数有一个引擎内部属性[[Scope]],函数创建时会在作用域链中填入一个全局对象(这里应该只是个名字而已,不是指这个对象是全局的变量),用来访问全局变量,函数运行时会建立一个运行期上下文(从书中的图里看起来,似乎[[Scope]]和运行期上下文是混用的?),运行期上下文有个激活对象,包含函数参数,this,和函数的局部变量的访问接口。

函数在访问变量的时候就是依据运行期上下文的作用域链进行查找的,其中访问全局变量是最远的,要注意对全局变量进行局部存储

with和try…catch可以临时改变运行期上下文的作用域链

使用with看似简化了代码,但是with会创建一个新的对象到运行期上下文中,这个对象就是with里面传入的参数,它被放到作用域链的第一个对象,这造成了一个问题,就是激活对象被放到了后一个作用域链对象里(推荐这里看看书上的图,便于理解),于是所有的局部变量和全局变量访问的时候就比原来远了一层

try…catch在执行catch时,会把异常对象放到作用域链的第一个对象,当catch执行完毕,作用域链会变回原来状态,可以通过把catch的处理包装成函数来避免局部变量访问路径长的问题

使用了with、try…catch、()的函数,被视为动态作用域,为什么()也算呢,摘录一段书中代码

function execute(code) {
    (code);
    function subroutine(){
    return window;
    }
    var w = subroutine();//但是很神奇的是我自己试了下这里还是返回全局的window
    //我还试了下给code传一个function也不行,用eval肯定是可以的
};

execute("var window = {};")//此时w的值,按书上的意思是,返回局部的window,然而。。。

闭包拥有和所在函数相同的[[Scope]],当闭包执行时,还会再创建一个闭包自身的激活对象,放在闭包所在函数的激活对象前面

3。DOM操作

没啥可说的,反正就是减少DOM操作,其实大家都知道,就是平时写的时候不注意,摘录一下代码,感受一下思想,

function innerHTMLLoop() {
    for (var count = 0; count < 15000; count++) {
        document.getElementById('here').innerHTML += 'a';
    }
}
function innerHTMLLoop2() {
    var content = '';
    for (var count = 0; count < 15000; count++) {
        content += 'a';
    }
    document.getElementById('here').innerHTML += content;
}

访问DOM的集合的时候,如果访问一组div的length,会比访问arr[div].length要慢,arr[div]是把这组div一个一个拷贝到js的一个数组里去

可以尝试使用children属性代替childNodes属性,注意childNodes返回 NodeList,而 children返回 HTMLCollection, querySelectorAll 返回的虽然是 NodeList,但是实际上是元素集合,并且是静态的(其他接口返回的HTMLCollection和NodeList都是动态的)

offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle()
这些属性和方法在调用时也会导致重排

三种方式批量操作DOM,比较推荐第二种
1。隐藏目标节点,操作完再展示
2。document.createDocumentFragment()
3。创建目标节点的副本

var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);

有很大的列表需要元素有点击事件时,考虑使用事件代理

浏览器不会在js执行的时候更新DOM,而是会把这些DOM操作存放在一个队列中,在js执行完之后按顺序一次性执行完毕,因此在js执行过程中用户一直在被阻塞。

4。算法和流程控制

for in 循环比for、while、do…while明显要慢

注意循环的时候用倒叙循环可以提升性能

for (var i=items.length; i--; ){//减少了一次比较和一次后执行
    process(items[i]);
}

Jeff Greenberg可以减少循环中的迭代次数,一般用不到,迭代次数超过1000量级才有必要使用

条件判断的分支非常多的时候,可以考虑查表法,根据实际情况尝试以下思路

var results = [result0, result1, result2, result3, result4, result5, result6, result7]
return results[value];

递归如果超过栈的上限可以用try…catch处理

function recurse(){
    recurse();
}
try {
    recurse();
} catch (ex){
    alert("Too much recursion!");
}

迭代的时候可以缓存计算结果,避免重复计算

function memfactorial(n){
    if (!memfactorial.cache){// 注意cache是挂在function上面的
    memfactorial.cache = {
        "0": 1,
        "1": 1
    };
    }
    if (!memfactorial.cache.hasOwnProperty(n)){
        memfactorial.cache[n] = n * memfactorial (n-1);
    }
    return memfactorial.cache[n];
}

封装的版本,fundamental就是要执行的函数,但是这个封装只能读取使用过的参数的结果(略鸡肋)

function memoize(fundamental, cache){
    cache = cache || {};
    var shell = function(arg){
        if (!cache.hasOwnProperty(arg)){
            cache[arg] = fundamental(arg);
        }
        return cache[arg];
    };
    return shell;
}

//举例
function fab(lim){
    if(lim==1){
        return 1;
    }else{
        return lim*fab(lim-1);
    }
}


var func = memoize(fab,{1:1,2:2});
func(1);
func(2);
func(4);//注意这里,由于fab是递归函数,所以没法使用1和2缓存的结果

5。字符串和正则表达式

可以用join的时候就不要用+连接字符串,在IE上有奇效

正则表达式的贪婪匹配是先尽可能多地匹配内容,然后从后往前进行回溯,非贪婪匹配是尽可能少地进行匹配,然后从前往后匹配,注意两者的区别,具体还是参看书上的例子为好

回溯失控,也是非常重要的内容,会推翻之前已经匹配好的内容尝试重新匹配,建议看书上的例子,解决方法,不用.*这种宽泛的匹配方式,用更具体的匹配方式减少回溯的影响

这里要插播一些正则表达式的相关语法,太多就单独写了一篇https://blog.csdn.net/u011393161/article/details/79930986

这里摘录一个书上匹配html的一个表达式的片段,我隐隐觉得以后可能有用
/<html>(?:(?!<head>)[\s\S])*<head>/
这个东西大概的意思是,前面是<html>,后面跟着的不能是<head>,但可以是换行和空白以及别的(英文和数字啥的),然后后面再跟一个<head>
如果我们拿'<html>asda sd<head>123123 <head>'去试,会得到'<html>asda sd<head>'。这一段匹配要58步 ̄へ ̄

书上对上面的这种写法做了优化,优化的过程没看懂,主要是讲回溯点丢弃的,记录一下优化后的形式
/<html>(?=([\s\S]*?<head>))\1

正则表达式如果匹配得慢,一般都是因为匹配失败慢,而不是匹配成功慢

尽量具体化匹配条件

分支尽量往后放,在分支前就过滤掉一部分内容,还有一些可替换的例子

cat|bat  [cb]at
red|read  rea?d
red|raw  r(?:ed|aw)
(.|\r|\n)  [\s\S]

当不需要捕获内容时,使用(?:...)替换(...)
如果确实需要匹配内容的一部分时,使用捕获而不是手动处理,比如需要获取"aha"中的aha,使用/"([^"]*)"/捕获而不是使用/"[^"]*"/然后自己处理引号(感觉后者一般不太会有人用)

需要重复使用一个正则表达式的时候,把它赋值给一个变量,避免正则表达式重复编译

尽量减少一个正则表达式所做的工作,如果可以拆分成两个,就拆分成两个,方便维护,第二个可以在第一个匹配的结果中进行匹配

分辨是否需要使用正则表达式,比如检查字符串是否以分号结束,/;$/.test(str)会先匹配分号,再匹配是否是结尾,可以用str.charAt(str.length - 1) == ";";代替,可以搭配slice、substr、substring、indexOf、lastIndexOf使用

记录一个代码,作用是字符串trim,效率上讲不推荐,但是思路值得参考

String.prototype.trim = function() {
    //先捕获第一个到最后一个非空字符之间的序列,存入$1,然后用$1替换整个字符串
    return this.replace(/^\s*([\s\S]*?)\s*$/, "$1");
}

接(2)https://blog.csdn.net/u011393161/article/details/80061389

猜你喜欢

转载自blog.csdn.net/u011393161/article/details/79790387