js文件加載優化

在js引擎部分,咱們能夠了解到,當渲染引擎解析到script標籤時,會將控制權給JS引擎,若是script加載的是外部資源,則須要等待下載完後才能執行。 因此,在這裏,咱們能夠對其進行不少優化工做。javascript

放置在body底部

爲了讓渲染引擎可以及早的將DOM樹給渲染出來,咱們須要將script放在body的底部,讓頁面儘早脫離白屏的現象,即會提前觸發DOMContentLoaded事件. 可是因爲在IOS Safari, Android browser以及IOS webview裏面即便你把js腳本放到body尾部,結果仍是同樣。 因此這裏須要另外的操做來對js文件加載進行優化.css

defer加載

這是HTML4中定義的一個script屬性,它用來表示的是,當渲染引擎遇到script的時候,若是script引用的是外部資源,則會暫時掛起,並進行加載。 渲染引擎繼續解析下面的HTML文檔,解析完時,則會執行script裏面的腳本。java

<script src="outside.js" defer></script>

他的支持度是<=IE9的.
而且,他的執行順序,是嚴格依賴的,即:react

<script src="outside1.js" defer></script>
<script src="outside2.js" defer></script>

當頁面解析完後,他便會開始按照順序執行 outside1 和 outside2文件。
若是你在IE9如下使用defer的話,可能會遇到 它們兩個不是順序執行的,這裏須要一個hack進行處理,即在兩個中間加上一個空的script標籤jquery

<script src="outside1.js" defer></script>
<script></script> //hack
<script src="outside2.js" defer></script>

可是,若是你將defer屬性用在inline的script腳本里面,在Chrome和FF下是沒有效果的。
即:webpack

<script type="text/javascript" defer = "defer">   //沒有效果
      console.log("defer doesn't make sense");
    </script>

async加載

async是H5新定義的一個script 屬性。 他是另一種js的加載模式。web

  1. 渲染引擎解析文件,若是遇到script(with async)ajax

  2. 繼續解析剩下的文件,同時並行加載script的外部資源數組

  3. 當script加載完成以後,則瀏覽器暫停解析文檔,將權限交給JS引擎,指定加載的腳本。瀏覽器

  4. 執行完後,則恢復瀏覽器解析腳本

能夠看出async也能夠解決 阻塞加載 這個問題。不過,async執行的時候是異步執行,形成的是,執行文件的順序不一致。即:

<script src="outside1.js" async></script>
<script src="outside2.js" async></script>

這時,誰先加載完,就先執行誰。因此,通常依賴文件就不該該使用async而應該使用defer.
defer的兼容性比較差,爲IE9+,不過通常是在移動端使用,也就不存在這個problem了。
其實,defer和async的原理圖,如圖同樣。(包括放在head中的script標籤)
此處輸入圖片的描述

腳本異步

腳本異步是一些異步加載庫(好比require)使用的基本加載原理. 直接上代碼:

function asyncAdd(src){
    var script = document.createElement('script');
    script.src = src;
    document.head.appendChild(script);
}
//加載js文件
asyncAdd("test.js");

這時候,能夠異步加載文件,不會形成阻塞的效果.
可是,這樣加載的js文件是無序的,沒法正常加載依賴文件。
若是你想要js文件按照你自定義的順序執行,則要將async設置爲false. 可是會阻塞其它文件的加載

var asyncAdd = (function(){
    var head = document.head,
        script;
    return function(src){
        script = document.createElement('script');
        script.src= src;
        script.async=false;
        document.head.appendChild(script);
    }
})();
//加載文件
asyncAdd("first.js");
asyncAdd("second.js");
//或者簡便一點
["first.js","second.js"].forEach((src)=>{async(src);});

可是,使用腳本異步加載的話,須要等待css文件加載完後,纔開始進行加載,不能充分利用瀏覽器的併發加載優點。而使用靜態文本加載async或者defer則不會出現這個問題。
使用腳本異步加載時,只能等待css加載完後纔會加載
此處輸入圖片的描述

使用靜態的async加載時,css和js會併發一塊兒加載
此處輸入圖片的描述
(from 妙淨)

關於這三種如何取捨,那就主要看leader給咱們目標是什麼,是兼容IE8,9仍是手機端,仍是桌面瀏覽器,或者兩兩組合。
可是對於單獨使用某一個技能的場景,使用時須要注意一些tips。
js文件放置位置應該放置到body末尾
若是使用async的話,最後加上defer以求向下兼容

<script src="test.js" async defer></script> //若是二者都支持,async會默認覆蓋掉defer
//若是隻支持一個,則執行對應的便可

一般,咱們使用的加載都是defer加載(由於很強的依賴關係).
但,上面的簡單js文件依賴加載只針對於,依賴關係不強,或者說,相互關聯性不強的js文件。先在js模塊化思想 已經成爲主流, 若是這樣手動添加defer或者async是沒有太大的實際意義的。
緣由就在於, 好複雜~
因此,纔有了webpack,requireJS等模塊打包工具。這也是給咱們在性能和結構上尋找一個平衡點的嘗試。
這裏也給你們安利一些建議:
業務邏輯代碼使用模塊化書寫, 測試代碼或者監聽代碼使用async,或者defer填充。 這也是比較好的實踐。

深刻腳本異步加載

最簡單的腳本異步就是在head裏添加一個script標籤.

var asyncAdd = (function(){
    var head = document.head,
        script;
    return function(src){
        script = document.createElement('script');
        script.async=false;
        document.head.appendChild(script);
    }
})();
asyncAdd("test.js"); //異步加載文檔

這樣寫,其實還不如,直接加async. 這樣簡單的異步加載,是不能知足咱們模塊化書寫的龐大業務邏輯的。 這裏,咱們將一步一步的優化咱們的代碼,實現,異步js文件加載的模塊化.

串行加載js文件

對上述簡單js異步腳本的升級版就是使用串行方式,加載js腳本。首先,咱們須要瞭解一下,DOMreadyState和onload事件,這裏先安利一下Nicholas大神 推薦的一份檢測onload的腳本:

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.body.appendChild(script);
}

但從IE11開始,已經支持onload事件, 不過,如今這份代碼的價值仍是很是大的, 目前主流兼容IE8+。
固然,咱們可使用loadScript中進行回調加載.

loadScript("test1.js",loadScript("test2.js",loadScript("test3.js")));

不過,這簡直就是沒人性的寫法。 因此,這裏咱們能夠進行優化一下。咱們可使用之前的模式,進行重構,這裏我選擇命令模式和鏈式調用。
直接貼代碼吧:

var loadJs = (function() {
        var script = document.createElement('script');
        if (script.readyState) {
            return function(url, cb) {
                script = document.createElement('script');
                script.src = url;
                document.body.appendChild(script);
                script.onreadystatechange = function() {
                    if (script.readyState == "loaded" ||
                        script.readyState == "complete") {
                        script.onreadystatechange = null; //解除引用
                        cb();
                    }
                };
            }
        } else {
            return function(url, cb) {
                script = document.createElement('script');
                script.src = url;
                document.body.appendChild(script);
                script.onload = function() {
                    cb();
                };
            }
        }
    })();

    //測試用例: commandJs.add("test.js",[test.js,test1.js]).exe();
    //或者 commandJs.add("test.js").add("test1.js").add([test1.js,test2,js]).exe();
    var commandJs = (function() {
        var group = [],
            len = 0;
        //類型檢測
        //數組
        var isArray = function(para) {
                return (para instanceof Array);
            }
            //String類型
        var isString = function(para) {
                return Object.prototype.toString.call(para) === "[object String]";
            }
            //集合檢測
        var correctType = function(para) {
                return isString(para) || isArray(para);
            }
            //添加src內容
        var add = function() {
            for (var i = 0, js; js = arguments[i++];) {
                if (!correctType(js)) {
                    throw new Error(`the ${i}th js file's type is not correct`);
                }
                group.push(js);
            }
            return this;
        }
        var isFinish = function() {
                len--;
                if (len === 0) {
                    exe(); //開始加載下一組js文件
                }
            }
            //並行加載js文件
        var loadArray = function(urls) {
            urls.forEach((url) => {
                loadJs(url, (function() {
                    isFinish(); //判斷是否執行徹底
                }).bind(this));
            });
        }
        var exe = function() {
            if (group.length === 0) return; //遍歷完全部的urls時,退出執行
            var js = group.shift();
            if (isArray(js)) {
                len = js.length;
                loadArray(js);
            } else {
                len = 1;
                loadArray([js]);
            }
            return this;
        }
        return {
            exe,
            add
        }
    })();

OK, 咱們來驗證同樣,串行執行的測試結果:

commandJs.add('./js/loader01.js').add('./js/loader02.js').exe();
//或者
commandJs.add('./js/loader01.js','./js/loader02.js').exe();
//這兩種寫法都是能夠的

最後的結果是:

ok~ 能夠經過,這樣能夠自定義加載不少依賴文件。 可是,形成結果是,時間成本耗費太大。 有時候, 一個主文件的main 有不少依賴js模塊, 那麼咱們考慮一下,可否把這些js模塊並行加載進來呢?

  1. 其實,上面的那一串代碼,已經將串行和並行給結合起來了。那並行是怎麼作的呢? 其實就是,同時向頁面中添加script tag而後監聽,是否全部的tag都已經加載完整。若是是,開始加載下一組js文件。

其實,最主要的代碼塊是這裏:

var isFinish = function() {
                len--;
                if (len === 0) {
                    exe(); //開始加載下一組js文件
                }
            }
            //並行加載js文件
        var loadArray = function(urls) {
            urls.forEach((url) => {
                loadJs(url, (function() {
                    isFinish(); //判斷是否執行徹底
                }).bind(this));
            });
        }
//執行順序就是,而後中間加了一些trick進行,類型的判斷.
//exe=> loadArray => isFinish ~>exe

並行加載js

OK, 上面咱們已經測試了js的異步加載,這裏咱們測試一下js並行加載的效果:

commandJs.add(['./js/loader01.js','./js/loader02.js']).exe();

上圖時間:

咱們對比一下異步加載的:

從上面很容易知道,異步和同步加載的區別,由於這個文件較小體現的價值不是很大,咱們換一個比較大的文件進行加載:

//並行:
 commandJs.add(['http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js','https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom-server.js']).exe();
 //串行
commandJs.add('http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js','https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom-server.js').exe();

看一下圖:
//並行
此處輸入圖片的描述
//串行

你們能夠鉤鉤手指,算一下二者的時間差, 一個是取max, 一個是取add. 結果是顯而易見的。 固然,模塊加載插件好比requireJS,labJS,他們所要作的功能比這裏的要豐滿的多, 當你 多個文件引入同一個依賴的時候,只須要加載一次(判斷惟一性), 以及引用模塊的ID 的 標識等。
js 腳本異步加載還有不少方法,好比xhr, iframe ,以及使用img 的 src進行加載,這些都是可行的, 可是他們的侷限性也很大, xhr,iframe的同域要求,使用img還不如直接使用script。 我這裏列一下他們的大概狀況表吧

加載方式 實現效果
xhr 腳本並行下載,要求同域,不會阻塞其餘資源
iframe 要求同域,腳本並行下載,不阻塞其餘資源,但損耗較大,目前業界推崇淘汰
img 慘無人道,你們知道有就好了

其實,你們看到這裏也就能夠了。下文,主要是我對上面代碼的一個優化,或者說是Promise實踐. 因爲懶得開篇幅了,因此就直接接着寫。

使用Promise異步加載

前面說了,若是使用像loadScript這種,直接進行回調串行的話,形成的結果是,callback hell;

loadScript("test1.js",loadScript("test2.js",loadScript("test3.js")));

若是瞭解Promise的童鞋,應該知道,使用Promise就能夠徹底解決這個問題。 這裏,咱們使用Promise對上面進行代碼進行重構

var loadJs = (function() {
        var script = document.createElement('script');
        if (script.readyState) {
            return function(url) {
                return new Promise(function(res, rej) {
                    script = document.createElement('script');
                    script.src = url;
                    document.body.appendChild(script);
                    script.onreadystatechange = function() {
                        if (script.readyState == "loaded" ||
                            script.readyState == "complete") {
                            script.onreadystatechange = null; //解除引用
                            res();
                        }
                    };
                })
            }
        } else {
            return function(url) {
                return new Promise(function(res, rej) {
                    script = document.createElement('script');
                    script.src = url;
                    document.body.appendChild(script);
                    script.onload = function() {
                        res();
                    };
                })
            }
        }
    })();

接着,咱們來調用代碼看看:

loadJs('http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js')
    .then(function(){
        return loadJs('./js/loader01.js');
    }).then(function(){
        console.log("finish loading");
    })

結果是:

那若是咱們想並行加載的話,怎麼辦呢? 很簡單使用Promise提供的all函數就能夠了.
show u the code:

Promise.all([loadJs('http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js'),loadJs('./js/loader01.js')])

結果爲:
OK~ 平時,咱們加載模塊的時候,就可使用Promise來進行練習,這樣能夠減小不少沒必要要的邏輯代碼。簡直,贊~\(≧▽≦)/~

相關文章
相關標籤/搜索