咱們寫程序的時候會常常遇到顯示進度的需求,如加載進度、上傳進度等。
最多見的實現方式是經過記錄 已完成數量(loadedCount)
和 總數量(totalCount)
,而後算一下就能獲得進度了。
這種方式簡單粗暴,容易實現,但很差擴展,必須有個地方維護全部 loadedCount
和 totalCount
。
本文將會基於上述實現方式,實現一種更容易擴展的進度管理方式。git
筆者在寫 WebGL 應用,在應用預加載階段須要計算加載進度。
加載的內容包括:模型資源、貼圖資源、腳本資源等。
其中模型資源中又會包含材質資源,材質資源裏面又會包含貼圖資源。
畫圖來表示的話就是以下的結構:github
+-------------------------------------------------------------+ | | | resources | | | | +----------+ +-----------------+ +-----------------+ | | | script1 | | model1 | | model2 | | | +----------+ | | | | | | | -------------+ | | -------------+ | | | +----------+ | |model1.json | | | |model2.json | | | | | script2 | | +------------+ | | +------------+ | | | +----------+ | | | | | | | +------------+ | | +------------+ | | | +----------+ | | material1 | | | | material1 | | | | | texture1 | | | +--------+ | | | | +--------+ | | | | +----------+ | | |texture1| | | | | |texture1| | | | | | | +--------+ | | | | +--------+ | | | | +----------+ | | +--------+ | | | | +--------+ | | | | | texture2 | | | |texture2| | | | | |texture2| | | | | +----------+ | | +--------+ | | | | +--------+ | | | | | +------------+ | | +------------+ | | | | | | | | | | +------------+ | | +------------+ | | | | | material2 | | | | material2 | | | | | +------------+ | | +------------+ | | | +-----------------+ +-----------------+ | | | +-------------------------------------------------------------+
這裏有個前提:當加載某個資源的時候,必須保證這個資源及它引用的資源所有加載完成後,才能算加載完成。
基於這個前提,咱們已經實現了一個 onProgress
接口,這個接口返回的進度是已經包含了子資源的加載進度的了。
翻譯成代碼就是:json
class Asset { load(onProgress) { return new Promise((resolve) => { if (typeof onProgress !== 'function') { onProgress = (_p) => { }; } let loadedCount = 0; let totalCount = 10; // NOTE: just for demo let onLoaded = () => { loadedCount++; onProgress(loadedCount / totalCont); if (loadedCount === totalCount) resolve(); }; Promise.all( this.refAssets.map(asset => asset.load().then(onLoaded)) ); }); } }
既然有了這個接口,若是沿用全局維護 loadedCount
和 totalCount
的形式的話,處理起來其實挺麻煩的。
本文接下來要介紹的,就是一種變通的作法。閉包
基本思想就是分而治之。把一個大任務拆分紅多個小任務,而後分別計算全部小任務的進度,最後再把全部小任務的進度歸併起來獲得總進度。
以下圖表示:測試
+--------------------------------------------------------------------+ | | | | | total progress | | | | +---------+---------+----------+----------+--------+--------+ | | | script1 | script2 | texture1 | texture2 | model1 | model2 | | | | (0~1) | (0~1) | (0~1) | (0~1) | (0~1) | (0~1) | | | +---------+---------+----------+----------+--------+--------+ | | | | model1 | | +-------------+-----------------------+-----------+ | | | model1.json | material1 | material2 | | | | (0~1) | (0~1) | (0~1) | | | +------------------------+------------------------+ | | | texture1 | texture2 | | | | (0~1) | (0~1) | | | +----------+------------+ | | | | model2 | | +-------------+-----------------------+-----------+ | | | model2.json | material1 | material2 | | | | (0~1) | (0~1) | (0~1) | | | +------------------------+------------------------+ | | | texture1 | texture2 | | | | (0~1) | (0~1) | | | +----------+------------+ | | | +--------------------------------------------------------------------+
基於這個原理去實現進度,實現方式就是經過一個列表去保存全部資源當前的加載進度,而後每次觸發 onProgress
的時候,執行一次歸併操做,計算總進度。this
var progresses = [ 0, // script1, 0, // script2, 0, // texture1, 0, // texture2, 0, // model1, 0, // model2 ]; function onProgress(p) { // TODO: progresses[??] = p; return progresses.reduce((a, b) => a + b, 0) / progresses.length; }
但這裏面有個難點,當觸發 onProgress
回調的時候,如何知道應該更新列表中的哪一項呢?
利用 JavaScript 的閉包特性,咱們能夠很容易實現這一功能。翻譯
var progresses = []; function add() { progresses.push(0); var index = progresses.length - 1; return function onProgress(p) { progresses[index] = p; reduce(); }; } function reduce() { return progresses.reduce((a, b) => a + b, 0) / progresses.length; }
利用閉包保留資源的索引,當觸發 onProgress
的時候,就能根據索引去更新列表中對應項的進度了。最後歸併的時候就能計算出正確的進度了。
剩下的事情就是整合咱們全部的代碼,而後對其進行測試了。code
咱們能夠用下面的代碼來模擬一下整個加載過程:索引
class Asset { constructor(totalCount) { this.loadedCount = 0; this.totalCount = totalCount; this.timerId = -1; } load(onProgress) { if (typeof onProgress !== 'function') { onProgress = (_p) => { }; } return new Promise((resolve) => { this.timerId = setInterval(() => { this.loadedCount++; onProgress(this.loadedCount / this.totalCount); if (this.loadedCount === this.totalCount) { clearInterval(this.timerId); resolve(); } }, 1000); }); } } class Progress { constructor(onProgress) { this.onProgress = onProgress; this._list = []; } add() { this._list.push(0); const index = this._list.length - 1; return (p) => { this._list[index] = p; this.reduce(); }; } reduce() { const p = Math.min(1, this._list.reduce((a, b) => a + b, 0) / this._list.length); this.onProgress(p); } } const p = new Progress(console.log); const asset1 = new Asset(1); const asset2 = new Asset(2); const asset3 = new Asset(3); const asset4 = new Asset(4); const asset5 = new Asset(5); Promise.all([ asset1.load(p.add()), asset2.load(p.add()), asset3.load(p.add()), asset4.load(p.add()), asset5.load(p.add()), ]).then(() => console.log('all resources loaded')); /** 輸出 Promise { <state>: "pending" } 0.2 0.3 0.36666666666666664 0.41666666666666663 0.45666666666666667 0.5566666666666668 0.6233333333333333 0.6733333333333333 0.7133333333333333 0.78 0.8300000000000001 0.8699999999999999 0.9199999999999999 0.96 1 all resources loaded */
這種方式的優勢是能避開全局管理 loadedCount
和 totalCount
,把這部分工做交回資源內部管理,它要作的只是對大任務進行歸併計算。接口
缺點也很明顯,須要對 onProgress
接口進行一次統一。在已有項目中推動難度很大,因此比較適合新項目或者小項目去實踐。
(完)。