做者:莫冠釗javascript
轉載請註明出處,保留原文連接和做者信息css
當今許多大型網頁應用尤爲是SPA
均採用了動靜分離
的策略。關於動靜分離的描述,這裏推薦一篇不錯的博文 網站靜態化處理—動靜分離策略。html
本人是作前端的,以前有幸與一位對性能追求極致的後端同窗一塊兒開發這種動靜分離的web項目,如下將從傳統順序模式
、單路數據併發模式(如下簡稱單併發模式)
、多路數據併發模式(如下簡稱多路併發模式)
來談談本身對這類應用關於前端加載
方面的心得。本文中的例子均來自該項目中。前端
通常狀況下,瀏覽器首先會接收到一張靜態的頁面,這張頁面會包含樣式文件和腳本文件引用的標籤(圖片什麼的不在這裏討論)。至於數據
哪裏來,下面介紹兩種方式:java
腳本請求獲取
一般,在腳本加載完畢後,腳本會執行一段向服務端發送請求數據的代碼,而後經過回調函數取出數據並作初始化工做。這一個過程爲:請求頁面
=> 渲染頁面
=> 加載腳本
=> 請求數據
=> 數據與腳本一塊兒初始化
=> 初始化完畢
,也就是從加載應用到啓動應用是以順序任務的形式執行。git
直接填充於隱藏標籤中
服務端也能夠直接將數據填充到網頁中的一個隱藏標籤中再傳回給客戶端,也就是上面順序中把獲取數據
放在頁面請求
以前。以後在腳本中直接去獲取相應的DOM
中的內容也就是數據,來進行初始化工做。github
這兩種方法各有優劣,由於不是本文重點,在此就直接帶過。不過筆者更傾向於前者。web
若是用工做流的思想去理解,大概能夠爲下圖(第一種方式):ajax
在這裏咱們只研究數據以及main.js的加載狀況。後端
base64.css是用來存儲一些小圖片的base64字符串而且是容許延後加載,能夠將其歸爲圖片資源一類。
整體狀況仍是能夠接受的,畢竟後端同窗對緩存這一塊下了很大的功夫,用戶會在500ms左右看到頁面的內容,到了600ms以後程序就能夠正式啓動。
這種模式的優勢是顯而易見的,這種順序加載啓動模式易用性、可維護性都比較好,也能很好地發揮動靜分離的特長。
然而,咱們認爲,若是將上圖中數據的請求放在前面和腳本一塊兒併發請求,也許會減小整個頁面的加載和啓動所需時間,並且後端同窗還以爲這樣的加載效果會更加直觀、整齊……
因而便有了下面的研究。
要實現數據與腳本併發加載,最核心的就是要讓數據不依賴於腳本進行加載,筆者所能想到的有兩種:
在頭部添加一個script
,插入一段發送ajax
請求的代碼,向服務端發送數據請求。
一樣是添加一個script
,將其src設爲數據請求的url
來引用外部數據資源。
單從執行效率來講,1比2還多了一步,故本文中選擇2進行討論。
把script在head標籤內。在下載script引入的外部腳本時,瀏覽器處於阻塞狀態,網絡很差或者script文件過大時,頁面處於空白停頓狀態,這樣的體驗是很很差的。
咱們通常會將腳本文件放在頁面底部來下降腳本下載與運行所帶來的阻塞影響,並且這樣能夠保證腳本中所引用的頁面元素已經渲染完畢。
而數據請求是與頁面元素無關,在這裏咱們但願它能放在頭部確保能夠儘早地開始加載來達到與其它資源一塊兒請求,但又不阻塞其餘資源的下載。
瀏覽器對標記有async屬性的scripts會當即加載並解析,該script相對於頁面的其他部分異步地執行(當頁面繼續進行解析時,腳本將被執行)。
這裏的解決辦法則是採用HTML5
的async
屬性,將其應用於數據請求相關的script上,就能夠達到腳本與數據併發加載的效果。以下代碼:
script(src="/Table/Data" type="text/javascript" async="async")
javascript
是一門解析性語言,當它加載完畢以後就會執行。
此時的數據請求變成了一個script標籤,也就是說,它能夠變成一段與賦值相關的javascript
代碼,直接把獲得的結果放在公共環境中。若是不把它變成賦值代碼,基於上面的引言,可能獲得的數據就會變成環境中的一個匿名對象
而在以後沒法再次被訪問。這樣一來,在腳本記載完畢就能夠直接去引用這個結果進行啓動頁面。那麼問題來了……
基於上面async
中闡述的方案,在實際中更多時候咱們可能沒法100%保證數據與腳本加載的前後順序。資源大小的確必定程度決定了加載時間,可是網絡傳輸也有着許多不穩定的因素。
咱們也不可能直接在任何一個script
中直接引用對方的資源(若是未加載完畢,會返回undefined
的錯誤)。
不到萬不得已,不該該使用輪詢檢查的方法去解決併發問題,這樣的應用性能過低,和咱們的初衷相違背。
既然它們是相互依賴的關係,並且咱們只須要其中一方引用另外一方的資源便可完成咱們所須要的啓動。在這裏,咱們只須要讓先加載完成前的把資源暴露到公共環境window
中,讓後加載的那一方察覺到以後直接引用進行啓動便可。
對於數據與腳本,咱們把它們的資源分別定爲:
名稱 | 資源 | 描述 |
---|---|---|
數據 | allData(Object) | 存儲全部的動態數據 |
腳本 | mainInitByData(Function) | 主引導函數 |
在數據請求裏,代碼爲:
var allData = window.allData = '{"name":"data"}'; //檢查腳本的資源是否存在 if (typeof window.mainInitByData !== 'undefined') { mainInitByData(JSON.parse(allData)); };
腳本里相關的片斷則爲:
var mainInitByData = window.mainInitByData = function(data) { //TODO... } if (typeof window.allData !== 'undefined') { mainInitByData(JSON.parse(allData)); }
不難發現,通過並行化處理以後,加載頁面的效率相比於以前的順序模式大大增長了。且頁面程序也能順利啓動(這裏你們能夠自行嘗試)。
不料後端同窗在一兩個月後,又提出了但願做多路數據併發請求,由於動態數據中也有部分數據相對一段時間內爲靜態的,這部分數據能夠用緩存處理,其餘數據則直接從其它服務器中獲取,能夠進一步提升併發效率。事情變得愈來愈有趣,也有了下面的研究。
此時,假設咱們所需請求的數據共有三條A、B、C,其中A爲相對靜態數據,能夠作出如下定義:
名稱 | 資源 | 描述 |
---|---|---|
子數據A | AData(Object) | 存儲A的相對靜態數據 |
子數據B | BData(Object) | 存儲B的動態數據 |
子數據C | CData(Object) | 存儲C的動態數據 |
腳本 | mainInitByData(Function) | 主引導函數 |
若是繼續沿用單併發中的策略,腳本的相關片斷代碼則爲:
var mainInitByData = window.mainInitByData = function(dataA, dataB, dataC) { //TODO... } if (typeof window.dataA !== 'undefined' && window.dataB !== 'undefined' && window.dataC !== 'undefined') { var dataA = JSON.parse(dataA), dataB = JSON.parse(dataB), dataC = JSON.parse(dataC); mainInitByData(dataA, dataB, dataC); }
以上數據只是一個例子,並不表明這樣就能夠解決這類的問題。假若有一天後端忽然要求一次併發加載10條數據,代碼就會變得十分冗餘。
既然要處理併發,那麼單併發的思想是能夠沿用的,只是這裏的方向不對。
不妨咱們換個角度思考,腳本仍然和數據進行互相檢查,可是這個數據包含了全部子數據,在這裏我直接將其稱爲父數據。那子數據之間怎麼辦?
之因此說是信號量的思想而不是信號量,由於信號量自己是多線程多任務同步,而對於帶有async標籤裏的javascript是單線程異步,但不表明javascript不能利用信號量的思想,信號量的思想就是在解決處理併發問題。具體的信號量定義,請讀者自行查閱。
爲了更好的描述這個借用思想的過程,先作如下定義:
父數據與子數據之間共用一種信號量,子數據運用這種信號量進行數據的整合,而父數據應用這種信號量進行與腳本初始化啓動。
每次子數據加載完畢後,釋放信號量,並把本身的數據整合到父數據中。
假設子數據之間申請信號量的順序未知,但一定在父數據以前。
整合的數據以及信號量都放在一個js對象integrateData
中,分別命名爲data
、sem
(其值爲1-子數據數量),即integrateData = {data: {}, sem: -2}
這裏可能須要對子數據的格式作必定的調整。變成如下類型,方便作整合
{"message":"success", "data": {....}}
那麼對於全部子數據的處理代碼爲:
var result = 'JSON'; var integrateData = window.integrateData || (window.integrateData = { data: {}, sem: 1 - 3 }); var onDataCallback = window.onDataCallback || (window.onDataCallback = function(result_, integrateData) { function dataIsReady(integrateData) { return integrateData.sem > 0; } function dataReadyCallback(integrateData) { integrateData.sem--; //父數據與腳本啓動 var mainInitBydata = window.mainInitBydata; if (typeof mainInitBydata === "function") { mainInitBydata(integrateData); } integrateData.sem++; } if (dataIsReady(integrateData)) { alert("非法請求"); return; } var result = result_; if (typeof result_ === "string") { result = JSON.parse(result_); } //數據整合 if (result.message === "success") { var data = result.data; for (var key in data) { integrateData.data[key] = data[key]; } } //釋放信號量 integrateData.sem++; //檢查信號量 if (dataIsReady(integrateData)) { dataReadyCallback(integrateData); } }); onDataCallback(result, integrateData);
此時,腳本里的相關代碼則爲:
var mainInitByData = window.mainInitByData = function(integrateData) { //TODO... } var integrateData = window.integrateData; //這裏無需擔憂衝突問題,由於js是單線程執行,子數據整合完畢後會直接執行父數據檢查腳本資源的行爲,因此sem>0時,父數據處於就緒狀態。 if (integrateData && integrateData.sem > 0) { mainInitBydata(integrateData) }
其實效率相比單併發提升很少,主要是涉及的動態數據規模不大,並且每次發送的請求報文和響應報文都會有必定大小的報頭,形成沒必要要的開銷。但假如動態數據足夠大的話,這種策略是能夠起到很大的做用。同時,單併發模式中的雙向檢查也能夠用信號量的思想實現。
總結以上的模式,咱們能夠得出如下的結論:
模式 | 效率 | 易用性 | 性能主要影響因素 | 適用場景 |
---|---|---|---|---|
順序 | 普通 | 容易 | 數據與程序的大小總和 | 通常的小項目 |
單併發 | 比順序模式高 | 普通 | 數據與程序大小比例 | 大多數動靜分離的網站應用 |
多路併發 | 通常比單併發高,當數據過小時效率會比單併發低 | 複雜 | 劃分數據的比例 | 數據比較龐大的網站應用,尤爲是數據之間按相對均勻的比例歸類 |
除此之外,上述中,單併發與多路併發的一大缺陷就是代碼的耦合性會相對地提升,對於多路併發而言,若是子數據請求之間有依賴關係,可能還要定義多種不一樣的信號量,不利於管理。
利用現有的工具好比EventProxy
,能夠很好管理這些併發請求,包括任務之間的依賴關係。經過事件訂閱與觸發的形式可讓程序更好地知道當前所完成的任務以觸發相應的回調函數進行處理。
但願本文能夠給讀者帶來必定的幫助。
最後打個小廣告,歡迎follow個人github:https://github.com/zero-mo