本文介绍了一种基于JavaScript闭包特性的进度管理方案。通过将大任务拆分为子任务,利用闭包保存资源索引,在onProgress回调中动态更新局部进度并归并计算总进度。该方案避免了全局变量维护的复杂性,适用于WebGL等资源预加载场景,但需统一接口标准。
前言
在程序开发过程中,显示加载或上传进度是常见需求。通常的做法是记录已完成的数量(loadedCount)和总数量(totalCount),通过计算得出当前进度。这种方式虽然简单直接且易于实现,但扩展性较差,因为必须有一个集中位置来维护所有的loadedCount和totalCount变量。本文将基于这种基础实现方式,探讨一种更具扩展性的进度管理方案。
问题
笔者在开发WebGL应用时,需要在预加载阶段计算整体加载进度。加载的资源类型多样,包括模型、贴图和脚本等。其中,模型资源可能包含材质资源,而材质资源又可能引用贴图资源,形成嵌套结构。这种层级关系可以用图示表示如下:
+-------------------------------------------------------------+
| |
| resources |
| |
| +----------+ +-----------------+ +-----------------+ |
| | script1 | | model1 | | model2 | |
| +----------+ | | | | |
| | -------------+ | | -------------+ | |
| +----------+ | |model1.json | | | |model2.json | | |
| | script2 | | +------------+ | | +------------+ | |
| +----------+ | | | | |
| | +------------+ | | +------------+ | |
| +----------+ | | material1 | | | | material1 | | |
| | texture1 | | | +--------+ | | | | +--------+ | | |
| +----------+ | | |texture1| | | | | |texture1| | | |
| | | +--------+ | | | | +--------+ | | |
| +----------+ | | +--------+ | | | | +--------+ | | |
| | texture2 | | | |texture2| | | | | |texture2| | | |
| +----------+ | | +--------+ | | | | +--------+ | | |
| | +------------+ | | +------------+ | |
| | | | | |
| | +------------+ | | +------------+ | |
| | | material2 | | | | material2 | | |
| | +------------+ | | +------------+ | |
| +-----------------+ +-----------------+ |
| |
+-------------------------------------------------------------+
这里有一个关键前提:只有当某个资源及其所有引用的子资源全部加载完成后,该资源的加载才算完成。基于此前提,我们已经实现了一个onProgress接口,该接口返回的进度值已经包含了子资源的加载情况。对应的代码实现如下:
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时,执行一次归并操作,计算总进度。具体实现代码如下:
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时,可以根据索引更新列表中对应项的进度。最后,在归并阶段计算出正确的总进度。剩下的工作就是整合所有代码并进行测试。
测试
我们可以使用以下代码来模拟整个加载过程:
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接口进行统一。在已有项目中推进这种方案难度较大,因此更适合在新项目或小型项目中实践。
以上就是JavaScript进度管理的详细内容,更多关于JavaScript进度管理的资料请关注本站其它相关文章!