JavaScript 進度管理

前言

咱們寫程序的時候會常常遇到顯示進度的需求,如加載進度、上傳進度等。
最多見的實現方式是經過記錄 已完成數量(loadedCount)總數量(totalCount),而後算一下就能獲得進度了。
這種方式簡單粗暴,容易實現,但很差擴展,必須有個地方維護全部 loadedCounttotalCount
本文將會基於上述實現方式,實現一種更容易擴展的進度管理方式。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))
            );
        });
    }
}

既然有了這個接口,若是沿用全局維護 loadedCounttotalCount 的形式的話,處理起來其實挺麻煩的。
本文接下來要介紹的,就是一種變通的作法。閉包

原理

基本思想就是分而治之。把一個大任務拆分紅多個小任務,而後分別計算全部小任務的進度,最後再把全部小任務的進度歸併起來獲得總進度。
以下圖表示:測試

+--------------------------------------------------------------------+
|                                                                    |
|                                                                    |
|   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 
 */

這種方式的優勢是能避開全局管理 loadedCounttotalCount,把這部分工做交回資源內部管理,它要作的只是對大任務進行歸併計算。接口

缺點也很明顯,須要對 onProgress 接口進行一次統一。在已有項目中推動難度很大,因此比較適合新項目或者小項目去實踐。

(完)。

出處

http://scarletsky.github.io/2...

相關文章
相關標籤/搜索