脚本的加载和执行

在打开一个站点的时候,浏览器会去加载各类资源。如今对JS的使用很是广泛,任何一个站点都会请求大量的JS脚本,而加载和执行的方式也是各不相同,但愿读完这篇文章能够对经常使用的加载和执行方式有一个总体的认识。javascript

首先介绍的是html中直接使用<script>标签。这也是咱们最多见的一种加载脚本的方式。html

// 直接使用script内嵌脚本
<script>
   document.getElementById("demo").innerHTML = "Hello JavaScript!";
</script>
// 使用script的src属性引用外部脚本
<script src="myscripts.js"></script>

这种方式在主流的浏览器能够是并行加载的,可是执行脚本的顺序仍是同步的,再加上浏览器是顺序解析页面,因此对于脚本的位置是有必定讲究的:java

  1. 若是脚本B须要使用到脚本A中的数据(好比fn或者变量),那脚本B必须放在脚本A后面。
  2. 由于执行引擎是单线程的,因此在执行JS的时候会阻塞DOM的渲染,致使页面长时间空白。若是不想的话能够考虑把JS放在文档的后面(好比body标签的底部)。
  3. 一个外链脚本就涉及到一个请求,再小的请求确定都会有性能开销,好比请求头,网络时延等等。因此在优化站点性能的时候减小外链也是要考虑的一个点。
【注】:可并行加载的浏览器包括:IE8+、firefox3.5+、safari4+和chrome2+。不一样浏览器对于同一个域名下的最大链接数有不一样的限制,基本在6个左右。具体能够参见 这篇文章

在程序的世界中,不少场景下同步阻塞就意味着性能问题。在这些场景下其实无阻塞脚本就能够搞定,主进程仍是去干当前最主要的事情,即加载和渲染DOM,而无阻塞的脚本能够等到页面加载完再去加载执行,这些场景正是性能优化的点。git


这就引伸出来了几种无阻塞脚本的方案。github

方案一:defer属性 【w3c】【MDN

clipboard.png

这个属性的是承诺用src引的脚本中不会修改DOM。放心的让这个脚本延迟执行吧。具体延迟到文档完成解析后,触发 DOMContentLoaded 事件前执行。chrome

【注1】执行是被延迟了,可是下载仍是根据script在页面中的位置。解析到时会去并行下载,可是不会执行。
【注2】由上述定义能够看出来需由src的存在,对于内嵌的脚本是无效的。
【注3】配了defer属性的脚本之间是按照顺序执行的

测试】 chrome 64.0.3282.119浏览器

<script src="./demo.js" defer></script>
   <script defer>
       console.log('script with defer');
   </script>
   <script>
       console.log('script withour defer');
   </script>
   <script>
       window.addEventListener("load",function (event) { 
           console.log("All resources finished loading!");
       });
       document.addEventListener("DOMContentLoaded", function(event) {
           console.log("DOM fully loaded and parsed");
       });
   </script>
</body>
// demo.js
console.log('inner demo.js');

结果:
clipboard.png性能优化

方案二:async属性 【w3c】【MDN

clipboard.png

配置了async属性是告诉浏览器,这个脚本异步去并行加载,加载完当即异步执行,可是加载的时机是不肯定的,因此这个属性比defer更开放。相关测试代码网络

【注1】由于异步加载完就当即异步执行,因此配了这个属性的脚本之间的关系也是不肯定的。因此 不能存在依赖async脚本内容的状况。
【注2】执行的时机智能肯定在load事件以前,和DOMContentLoaded的时机不能肯定
【注3】优先级是高于defer的
【注4】和defer同样,对内嵌脚本无效;不能有 document.write改写dom的代码

方案三:动态脚本

动态脚本是咱们比较经常使用的异步加载和执行JS的方式。这种实现要特别注意浏览器的兼容性。简单的实现方式以下:app

function loadJs(url,callback) {
    var callback = callback || (() => {});
    var script = document.createElement('script');
    script.type = "text/javascript";
    script.src = url;

    if(script.readyState){ //IE
        script.onreadystatechange = function () {
            if(script.readyState == "loaded" || script.readyState == 'complete'){
                console.log('inner onreadystatechange');
                script.onreadystatechange = null;
                callback();
            }
        };
    } else {
        script.onload = function () {
            console.log('inner onload');
            callback();
        };
    }

    document.getElementsByTagName("head")[0].appendChild(script); // 开始下载并执行
}

loadJs("./server.js");

这种建立的方式,文件在该元素被添加到页面时开始下载,加载完开始执行,而且文件的下载和执行过程不会阻塞其它进程。能够认为这种建立默认加了async属性。咱们还能够经过设置async = false的方式取消异步的特性。正由于这个特性,绝大多数场景下都是有益的,可是当咱们想使用这种方式去加载多个JS时,而且有前后顺序的时候,能够尝试在callbak里去迭代发请求。

loadJs('f1.js',()=>{
    loadJs('f2.js',()=>{
        loadJs(xxx);
    })
});

方案四:XMLHttpRequest(XHR)注入脚本

搞过Ajax的对XHR应该都很熟悉了,在这就不详细介绍了,须要的去Google一把。

XHR主要是请求脚本,而后咱们能够控制请求回来的脚本,在咱们须要的时候经过上述动态建立脚本的方式注入到页面中。这种方式最大的好处就是兼容性好。弊端也很明显,必须同源。下面是一个简单实现,没有考虑在建立xhr的兼容性,好比ActiveXObject,有须要的能够去google一把:

var xhr = new XMLHttpRequest(); // 建立xhr对象
xhr.open("get",'./server.js',true); // 初始化一个请求, 支持CRUD
xhr.onreadystatechange = function () { 
    if(xhr.readyState == 4){
        if(xhr.status >= 200 && xhr.status < 3000 || xhr.status == 304){
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.text = xhr.responseText;
            document.body.appendChild(script);
        }
    }
}
xhr.send(null); // 发送请求

这地方说明一下xhr.status:

状态 描述
0 UNSENT (未打开) open()方法还未被调用.
1 OPENED (未发送) open()方法已经被调用.
2 HEADERS_RECEIVED (已获取响应头) send()方法已经被调用, 响应头和响应状态已经返回
3 LOADING (正在下载响应体) 响应体下载中; responseText中已经获取了部分数据.
4 DONE (请求完成) 整个请求过程已经完毕.

其它方案:

document.write

对于document.write,通常都不推荐使用的,主要是由于存在write方法的脚本可能会在解析的过程当中修改DOM,致使一些脚本没法预加载,甚至会致使一些已经预解析和预加载失效。网上也不少关于为何要避免使用document.write的文章,感兴趣的能够去google一把。

innerHtml

对于innerHtmlouterHTML, 只会以字符串的形式来承载,不会去执行对应的脚本的。


总结:上文主要介绍了动态建立脚本和XHR的方式去建立异步加载和执行脚本的方式。在某些性能调优的状况下仍是颇有用的,而XHR更是Ajax的核心。


参考

高性能Javascript

相关文章
相关标签/搜索