在js引擎部分,咱們能夠了解到,當渲染引擎解析到script標籤時,會將控制權給JS引擎,若是script加載的是外部資源,則須要等待下載完後才能執行。 因此,在這裏,咱們能夠對其進行不少優化工做。javascript
爲了讓渲染引擎可以及早的將DOM樹給渲染出來,咱們須要將script放在body的底部,讓頁面儘早脫離白屏的現象,即會提前觸發DOMContentLoaded事件. 可是因爲在IOS Safari, Android browser以及IOS webview裏面即便你把js腳本放到body尾部,結果仍是同樣。 因此這裏須要另外的操做來對js文件加載進行優化.css
這是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是H5新定義的一個script 屬性。 他是另一種js的加載模式。web
渲染引擎解析文件,若是遇到script(with async)ajax
繼續解析剩下的文件,同時並行加載script的外部資源數組
當script加載完成以後,則瀏覽器暫停解析文檔,將權限交給JS引擎,指定加載的腳本。瀏覽器
執行完後,則恢復瀏覽器解析腳本
能夠看出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腳本。首先,咱們須要瞭解一下,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模塊並行加載進來呢?
其實,上面的那一串代碼,已經將串行和並行給結合起來了。那並行是怎麼作的呢? 其實就是,同時向頁面中添加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
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實踐. 因爲懶得開篇幅了,因此就直接接着寫。
前面說了,若是使用像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來進行練習,這樣能夠減小不少沒必要要的邏輯代碼。簡直,贊~\(≧▽≦)/~