高性能JavaScript笔记一(加载和执行、数据访问、DOM编程)

写在前面

好的书,可能你第一遍并不能领会里面的精魂,当再次细细品评的时候,发现领悟的又是一层新的含义

(这段时间,工作上也不会像从前一样做起来毫不费力,开始有了新的挑战,现在的老大让我既佩服又嫉妒,但真的是打心底里仰慕,希望自己有朝一日能过到他那个高度)

既然现在还达不到那个层次,就好好堆砖吧,当砖堆到一定高度也自然会上一个小台阶。

脚本位置

脚本会阻塞页面渲染,直到它们全部下载并执行完成,页面才会继续渲染。页面只有加载并执行完前面一个script外部文件才会去加载下面一个script标签。

在IE8、FireFox3.5、Safari4、和Chrome2才开始允许并行下载JavaScript文件,也就是说从这个时候开始script标签在下载外部资源时不会阻塞其它script标签,但是javascript下载的过程仍然会阻塞其它资源的下载(比如图片),尽管脚本的下载过程不会互相影响,但页面仍然必须等待所有的javascript代码下载并执行完成才能继续

这也就是为什么推荐将所有的script标签尽可能放到body标签的底部,以尽量减少对整个页面下载的影响

组织脚本

由于每个script标签初始下载时都会阻塞页面渲染,所以减少页面包含的script标签数量能改善页面性能(这不仅仅针对外链脚本,内嵌脚本的数量也同样限制)

把一段内嵌脚本放在引用外链样式表的link标签之后会导致页面阻塞去等待样式表的下载,浏览器这样做是为了确保内嵌脚本在执行时能获得最精确的样式信息,因此,建议永远不要把内嵌脚本紧跟在link标签后面

把多个文件合并为一个,可以减少性能消耗 

无阻塞脚本

无阻塞脚本也就是在页面加载完成后才加载javaScript代码,有多种方式来实现

1、script标签有一个扩展属性defer,defer属性指明本元素所含脚本不会修改dom,因此代码能安全的延迟执行(但是它并非所有浏览器兼容)

带有defer属性的script标签可以放置在文档的任何位置,对应的javascript文件将在页面解析到script标签时开始下载,但并不会执行,直到dom加载完成(不仅仅外链脚本,内嵌脚本也一样)

也就是说带有defer属性的javascript文件下载时,它不会阻塞浏览器的其它进程,因此它可以与页面的其它资源并行下载

2、动态脚本元素:因为script元素与页面其它元素没有实质性的差别,利用这点,可以通过js创建script并添加到页面上,这样做的好处在于:无论何时启动下载,文件的下载和执行过程都不会阻塞页面其它进程,你甚至可以把代码放到页面head区域而不会影响页面其它部分

通常来讲,把新创建的script标签添加到head标签里比添加到body里更保险 ,尤其是在页面加载过程中执行代码时更是如此,因为body中的内容没有全部加载完成,这时... 

使用动态脚本节点下载文件后,返回的代码通常会立即执行(这样也会有一个问题,所以你必须确保代码中需要调用的接口都已经准备就绪)

script元素提供一个readyState属性,它有5种取值

  • uninitialized初始状态
  • loading开始下载
  • loaded下载完成
  • interactive数据完成下载但尚不可用
  • complete所有数据已准备就绪
        /*
        *@desc:兼容所有浏览器的动态加载script
        *@param:url  script的src
        *@param:callback  回调函数
        */
        function loadScript(url, callback) {
            var script = document.createElement('script');
            script.type = 'text/javascript';
            if (script.readyState) {//IE
                script.onreadystatechange = function () {
                    //IE在标识最终状态readyState的值时并不一致,有时到达loaded状态而不到达complete,有时候甚至不经过loaded就到达complete状态,
                    //所以靠谱的方式就是同时检查这两种状态
                    if (script.readyState = 'loaded' || script.readyState == 'complete') {
                        script.onreadystatechange = null;
                        callback();
                    }
                }
            } else {
                script.onload = function () {
                    callback();
                }
            }
            script.src = url;
            document.getElementsByTagName('head')[0].appendChild(script);            
        }

  使用如上代码的方法动态加载js,浏览器不会保证执行的顺序

3、使用XMLHttpRequest(XHR),也就是通过创建一个XHR通过,然后用它下载js文件,最后动态创建script元素将代码注入页面中

        var xhr = new XMLHttpRequest();
        xhr.open('get', 'file1.js', true);
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {//2XX表示有效响应  304表示从缓存中读取
                    var script = document.createElement('script');
                    script.type = 'text/javascript';
                    script.text = xhr.responseText;
                    document.body.appendChild(script);
                }
            }
        }
        xhr.send(null);

使用XHR的特性:

  • 下载javascript代码,但不立即执行
  • 同样的代码在所有浏览器中都能正常使用
  • 请求的js文件必须与所请求的页面牌相同的域

推荐的无阻塞模式

    <script>
        /*
        *@desc:兼容所有浏览器的动态加载script
        *@param:url  script的src
        *@param:callback  回调函数
        */
        function loadScript(url, callback) {
            var script = document.createElement('script');
            script.type = 'text/javascript';
            if (script.readyState) {//IE
                script.onreadystatechange = function () {
                    //IE在标识最终状态readyState的值时并不一致,有时到达loaded状态而不到达complete,有时候甚至不经过loaded就到达complete状态,
                    //所以靠谱的方式就是同时检查这两种状态
                    if (script.readyState = 'loaded' || script.readyState == 'complete') {
                        script.onreadystatechange = null;
                        callback();
                    }
                }
            } else {
                script.onload = function () {
                    callback();
                }
            }
            script.src = url;
            document.getElementsByTagName('head')[0].appendChild(script);
        }
        //将loadScript函数直接嵌入页面,从而避免产生一次http请求
        loadScript('theRest.js', function () {
            //加载初始化页面所需的剩下的代码
            //好处1)确保js执行过程中不会阻塞页面其它内容的显示
                //2)当第二个js文件完成下载时,应用所需的所有dom结构已创建完毕,并做好了交互的准备,从而避免了需要另一个事件
        });
    </script>
View Code

 可以去了解一下LazyLoad类库,它是一个更为通用的延迟加载工具

看看我的另一篇博文  LABjs(类似于LazyLoad,但它更加方便管理依赖关系)

总结延迟加载

数据访问相关性能优化法则

数据的存储位置关系到访问速度 

1、如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量中

2、有两个语句可以在执行时临时改变作用域链,一个是with,一个是catch(深入理解了作用域链后就会发现with会给对象的所有属性重新创建一个变量并把它推放到作用域的最顶端,那么原先对象的属性就就位于作用域链的第二层,这样访问局部变量启不是变慢了) 避免使用with语句

3、catch同with原理差不多,如果代码发生错误,执行过程会自动跳转到catch子句,然后把异常对象推入一个可变对象并置于作用域的头部,在catch代码块内部,函数所有局部变量将会被放到第二个作用域链对象中,一个很好的模式是将错误委托给一个函数处理(catch子句执行完毕,作用域链就会返回到原先的状态)

        try {
            methodMayCauseError();
        } catch (ex) {
            handlerError(ex);//委托给处理器方法
            //由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能
        }

4、无论是with、try-catch、eval都被认为是动态作用域,因为动态作用域只存在于代码执行过程中。因此无法通过静态分析检测出来。所以只有确实有必要的时候才推荐使用动态作用域

5、闭包允许函数访问局部作用域之外的数据,但也因此引发了内存性能问题(同样可以使用局部变量的方式将常用的跨作用域变量存储起来)

6、对象在原型链中存在的位置越深,找到它也就越慢。同理对象成员嵌套越深,访问速度就会越慢。这也就是为什么多次读取同一个对象属性,最佳做法是将属性值保存在局部变量中

(通过点表示法object.name操作和通过括号表示法object['name']操作事实上性能没有明显区别,只有在safari中,点符号始终要快)

总结数据访问

DOM编程

因为浏览器内部机制是把DOM和JavaScript独立实现的,这样也就说明两个相互独立的功能只能通过接口彼此连接,就会产生消耗

修改页面区域的最佳方案是使用innerHTMl比document.createElement方法稍微会快些

使用element.cloneNode替代document.createElement方法会更有效率,但也不是特别明显

        document.getElementsByName();
        document.getElementsByClassName();
        document.getElementsByTagName();
        document.images;
        document.links;
        document.forms;
        document.forms[0].elements;

上面代码中返回的对象都是HTML集合对象,它们是处于一种实时状态的,也就是说它一直与文档保持连接,每次你需要最新信息时,都会重复执行查询的过程(即使你只是访问集合的length属性)   同样的道理,访问集合元素时也最好把集合存储在局部变量中,并把length缓存在循环外部

        function toArray(coll) {
            for (var i = 0,a=[],len=coll.length; i < len; i++) {
                a[i] = coll[i];
            }
            return a;
        }

在dom中查找元素时,用的最多的恐怕就是childNodes和nextSibling,但如果你的浏览器需要兼容IE的话,推荐使用nextSibling方法来查看DOM节点

DOM属性诸如childNodes、firstChild、nextSibling都不区分元素节点和其它类型的节点,比如注释和文本节点,很多情况下,只需要访问元素节点,因此在循环中需要检查返回节点的类型并过滤掉非元素节点,但这些类型检查都是不必要的DOM操作,使用children替代childNodes会更快

如果浏览器具有原生的QuerySelectorAll()方法和querySelector()方法尽量使用它们(因为自己实现它们的功能需要编写特别多的代码,还有就是任何编程语言都有一个共通的特性就是原生方法永远是性能最佳的)

重排与重绘

当DOM变化影响到元素的几何属性时,浏览器就需要重新计算元素的几何属性,同样其它元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失控,并重新构造渲染树,这个过程叫重排,完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程叫重绘

重排什么时候发生呢?

  • 添加或删除可见的DOM元素
  • 元素位置改变
  • 元素尺寸改变
  • 内容改变
  • 页面浏览器初始化
  • 浏览器窗口尺寸改变

需要注意的是访问以下属性,会触发重排(所以在修改样式的过程中,最好避免使用上面列出的属性)

最小化重排与重绘

合并所有改变然后一次处理,这样就只修改dom一次,1)使用cssText属性来实现 2)修改css的类名称来更改

缓存布局信息(比如说偏移量offsets、滚动位置或计算出的样式值computedstyle values  尽量减少布局信息的获取次数,获取后赋值给局部变量,然后再操作局部变量)

通过以下步骤来减少重排与重绘的次数

  1. 从文档流中摘下此元素
  2. 对其应用多重改变
  3. 将元素带回文档中

有三种方法可以使dom脱离文档

  1. 隐藏元素,对其进行修改后再显示
  2. 使用文档片断在已存DOM之外创建一个子树,然后将它拷贝到文档中(推荐使用此方法,因为它是涉及最小数量的DOM操作和重排)
  3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后然后覆盖原始元素
for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            document.body.appendChild(el);
        }
        //可以替换为:
        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

如果是动画元素的话,最好使用绝对定位以让它不在文档流中,这样的话改变它的位置不会引起页面其它元素重排(也就是说如果是动画元素的话最好是使用绝对定位以让它不在文档流中,这样的话改变它的位置不会引起页面其它元素重排,只会影响它自己重排。

IE下如果你有大量元素使用了:hover,那么会降低响应速度 

DOM编程小结

posted @ 2015-06-22 13:25  静逸  阅读(938)  评论(2编辑  收藏  举报