Detailed explanation of JavaScript progress management

Detailed explanation of JavaScript progress management

Preface

When we write programs, we often encounter the need to display progress, such as loading progress, uploading progress, etc.
The most common implementation method is to record the completed quantity (loadedCount) and the total quantity (totalCount), and then calculate the progress.
This method is simple and crude, easy to implement, but difficult to expand, and there must be a place to maintain all loadedCount and totalCount.
This article will implement a more easily scalable progress management method based on the above implementation method.

question

I am writing a WebGL application and need to calculate the loading progress during the application preloading phase.
The loaded content includes: model resources, map resources, script resources, etc.
The model resources will contain material resources, and the material resources will contain texture resources.
If we draw a picture to represent it, the structure is as follows:

+-------------------------------------------------------------+
| |
| resources |
| |
| +----------+ +-----------------+ +-----------------+ |
| | script1 | | model1 | | model2 | |
| +----------+ | | | | |
| | -------------+ | | -------------+ | |
| +----------+ | |model1.json | | | |model2.json | | |
| | script2 | | +------------+ | | +------------+ | |
| +----------+ | | | | |
| | +------------+ | | +------------+ | |
| +----------+ | | material1 | | | | material1 | | |
| | texture1 | | | +--------+ | | | | +--------+ | | |
| +----------+ | | |texture1| | | | | |texture1| | | |
| | | +--------+ | | | | +--------+ | | |
| +----------+ | | +--------+ | | | | +--------+ | | |
| | texture2 | | | |texture2| | | | | |texture2| | | |
| +----------+ | | +--------+ | | | | +--------+ | | |
| | +------------+ | | +------------+ | |
| | | | | |
| | +------------+ | | +------------+ | |
| | | material2 | | | | material2 | | |
| | +------------+ | | +------------+ | |
| +-----------------+ +-----------------+ |
| |
+-------------------------------------------------------------+

There is a premise here: when loading a resource, it must be ensured that the resource and the resources it references are all loaded before the loading is considered complete.
Based on this premise, we have implemented an onProgress interface, which returns the progress that already includes the loading progress of sub-resources.
Translated into code:

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))
            );
        });
    }
}

Now that we have this interface, if we continue to use the form of global maintenance of loadedCount and totalCount, it will be quite troublesome to handle.
What this article will introduce next is a workaround.

principle

The basic idea is divide and conquer. Split a large task into multiple small tasks, then calculate the progress of all small tasks separately, and finally merge the progress of all small tasks to get the total progress.
As shown in the following figure:

+--------------------------------------------------------------------+
| |
| |
| 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) | |
| +----------+------------+ |
| |
+--------------------------------------------------------------------+

Based on this principle, the progress is implemented by saving the current loading progress of all resources through a list, and then performing a merge operation each time onProgress is triggered to calculate the total progress.

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;
}

But there is a difficulty here. When the onProgress callback is triggered, how do we know which item in the list should be updated?
By using the closure feature of JavaScript, we can easily implement this function.

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;
}

Use closures to retain the index of resources. When onProgress is triggered, the progress of the corresponding item in the list can be updated according to the index. The correct progress can be calculated during the final merge.
All that’s left is to integrate all our code and test it.

test

We can use the following code to simulate the entire loading process:

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'));

/**
  Output Promise { <state>: "pending" }
  
  0.2 
  0.3 
  0.366666666666666664 
  0.416666666666666663 
  0.456666666666666667 
  0.55666666666666668 
  0.6233333333333333 
  0.6733333333333333 
  0.7133333333333333 
  0.78 
  0.8300000000000001 
  0.8699999999999999 
  0.9199999999999999 
  0.96 
  1 
  all resources loaded 
 */

The advantage of this method is that it can avoid the global management of loadedCount and totalCount, and return this part of the work to the internal management of resources. All it has to do is merge and calculate large tasks.

The disadvantages are also obvious, and the onProgress interface needs to be unified. It is very difficult to advance in existing projects, so it is more suitable for new projects or small projects.

The above is the details of JavaScript progress management. For more information about JavaScript progress management, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • js realizes the progress process with arrows
  • JS realizes dynamic loading effects of progress bar
  • JavaScript+css to achieve progress bar effect
  • JS implements a controllable progress bar
  • js to achieve a simple progress bar effect
  • Node.js implements multiple file uploads with progress bar
  • js+HTML5 canvas to implement a simple loading bar (progress bar) function example
  • Teach you to use native js to implement a file upload preview component with progress monitoring in 3 minutes
  • Code for implementing a download progress bar and a playback progress bar in JS

<<:  MySQL 5.7.21 winx64 installation and configuration method graphic tutorial under Windows 10

>>:  Implementation of Nginx operation response header information

Recommend

Detailed explanation of the seven data types in JavaScript

Table of contents Preface: Detailed introduction:...

MySQL replication mechanism principle explanation

Background Replication is a complete copy of data...

Steps to install RocketMQ instance on Linux

1. Install JDK 1.1 Check whether the current virt...

JavaScript+html to implement front-end page sliding verification (2)

This article example shares the specific code of ...

Explanation of Truncate Table usage

TRUNCATE TABLE Deletes all rows in a table withou...

4 Ways to Quickly Teach Yourself Linux Commands

If you want to become a Linux master, then master...

js tag syntax usage details

Table of contents 1. Introduction to label statem...

How to change the default character set of MySQL to utf8 on MAC

1. Check the character set of the default install...

MySQL whole table encryption solution keyring_file detailed explanation

illustrate MySql Community Edition supports table...

React ref usage examples

Table of contents What is ref How to use ref Plac...

Mysql queries the transactions being executed and how to wait for locks

Use navicat to test and learn: First use set auto...

Detailed code for adding electron to the vue project

1. Add in package.json "main": "el...

How to change password and set password complexity policy in Ubuntu

1. Change password 1. Modify the password of ordi...