JavaScript has only a single thread of execution. Everything you see that makes it look like web apps can do multiple things at once is a complete fiction, built of smoke and mirrors. The original Macintosh was like this, and old school Mac programmers (like me) remember WaitNextEvent
. You had to call this system function frequently, or your app would appear to lock up the entire computer. Behind the scenes, WaitNextEvent
would pause your app and allow other applications (and the OS) to take control of the CPU and do stuff.
So what this really comes down to is how to structure your big loop. The mechanics of “how do I update the view?” is not where I would recommend you focus your effort. If you would describe in more detail what this big loop is doing, we could give more targeted recommendations, but for now I can only speak in generics. We’re going to walk through a big cube, rendering voxels.
interface Point3 {
x: number;
y: number;
z: number;
}
type Cube = Point3;
The single smallest unit of work we can do is to render a single voxel:
renderVoxel(pt: Point3): void {
// do stuff
}
So here’s a naive implementation that “stops the world” while rendering the whole cube:
advance(pt: Point3, world: Cube): Point3 | null {
let rv = {x: pt.x + 1, y: pt.y, z: pt.z};
if (rv.x >= world.x) { rv.x = 0; ++rv.y; }
if (rv.y >= world.y) { rv.y = 0; ++rv.z; }
if (rv.z >= world.z) { rv = null; }
return rv;
}
renderWorld(world: Cube): void {
let pt: Point3 = { x: 0, y: 0, z: 0 };
while (pt) {
this.renderVoxel(pt);
pt = this.advance(pt);
}
}
Now let’s try to add progress feedback:
volume(cube: Cube): number {
return cube.x * cube.y * cube.z;
}
percentDone(pt: Point3 | null, world: Cube): number {
return pt ? (this.volume(pt) / this.volume(world) * 100) : 100;
}
progress = 0;
renderAndAdvance(pt: Point3, world: Cube): Point3 | null {
this.renderVoxel(pt);
let rv = this.advance(pt);
this.progress = this.percentDone(rv, world);
return rv;
}
renderWorld(world: Cube): void {
let pt: Point3 | null = { x: 0, y: 0, z: 0 };
while (pt) {
pt = this.renderAndAdvance(pt, world);
}
}
UPDATE: actually, that volume computation won’t work for the partials, but I’m leaving it as broken for now because it’s not essential to the main point of the thread
You’ve probably gotten to more or less this point, where judicious console.log
usage will watch progress
going up, but can’t figure out how to reflect this in the UI. To do so, we have to stop stopping the world:
renderAndAdvance(pt: Point3, world: Cube): Promise<Point3 | null> {
return new Promise(resolve => setTimeout(() => {
this.renderVoxel(pt);
let rv = this.advance(pt);
this.progress = this.percentDone(rv, world);
resolve(rv);
}, 0));
}
renderWorld(world: Cube): void {
let upto: Point3 | null = { x: 0, y: 0, z: 0 };
let voxr$ = Promise.resolve(pt);
while (upto) {
voxr$ = voxr$.then(pt => {
// yes, virginia, there is only supposed to be one '=' in that condition
return (upto = pt) ? this.renderAndAdvance(pt, world) : null;
});
}
}