Yes, it will block. Promise is not some kind of magic dust; sprinkling it over blocking code doesn't suddenly make it non-blocking.
In your code new Promise
is actually redundant: as you already declared your function as async, it already returns a promise all the time. And yes, as your original Promise Executor is synchronous (you iterate over your list with plain for ... of
), the rest of your code has to wait till the commands' queue there is exhausted:
const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));
const determineEvenCount = async (list) => {
console.time('Inside Loop: ' + list.length);
let evenCount = 0;
for (let elem of list) {
if (elem % 2 === 0) {
evenCount++;
}
}
console.timeEnd('Inside Loop: ' + list.length);
console.log(evenCount);
return evenCount;
};
console.time('Timer execution');
setTimeout(() => {
console.timeEnd('Timer execution');
}, 5);
Promise.resolve().then(() => {
console.log('Microtask execution');
});
console.time('Waiting for sync');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for sync');
As you can see, lists were iterated in sequence (larger list before smaller, even though the latter clearly took less time), and both timers and promises were fired afterwards. Totally blocking.
Still, there's a really simple way to make this function less blocking:
const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));
const determineEvenCount = async (list) => {
console.time('Inside Loop: ' + list.length);
let evenCount = 0;
for await (let elem of list) {
if (elem % 2 === 0) {
evenCount++;
}
}
console.timeEnd('Inside Loop: ' + list.length);
console.log(evenCount);
return evenCount;
};
console.time('Timer execution');
setTimeout(() => {
console.timeEnd('Timer execution');
}, 5);
Promise.resolve('Microtask execution A').then(console.log);
console.time('Waiting for async');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for async');
Promise.resolve('Microtask execution B').then(console.log);
... with results looking like this:
Waiting for async: 0.075ms
Microtask execution A
Microtask execution B
Inside Loop: 18.555ms
5000
50000
Timer execution: 46.400ms
As you can see, not only the wrapping timer executed instantly, but also both Promises have resolved before your array was processed. All's rosy, right?
Nope. We stayed in the same loop (kudos for @Kaiido for pointing that part out), which means not only the timers were blocked for the whole set, but also no other tasks (I/O processing in particular) were able to execute. Yet the processing time increased significantly, as fetching of each separate element of that list was delayed.
That's why what you most likely should look to is chunking the processing first, and using setImmediate
for delaying each chunk. For example (using setTimeout here to emulate setImmediate, just to show the idea):
const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));
const setImmediate = (fn) => {
setTimeout(fn, 0);
};
const determineEvenCount = async (list) => {
console.time('Inside Loop: ' + list.length);
return new Promise((resolve) => {
let evenCount = 0;
function counter(elem) {
if (elem % 2 === 0) {
evenCount++;
}
}
const CHUNK_SIZE = 100;
! function processChunk(start, end) {
const boundary = Math.min(end, list.length);
let i = start;
while (i < boundary) {
counter(list[i++]);
}
if (i === list.length) {
console.timeEnd('Inside Loop: ' + list.length);
console.log(evenCount);
return resolve(evenCount);
}
setImmediate(() => processChunk(i, i + CHUNK_SIZE));
}(0, CHUNK_SIZE);
});
console.timeEnd('Inside Loop: ' + list.length);
};
console.time('Timer execution');
setTimeout(() => {
console.timeEnd('Timer execution');
}, 5);
Promise.resolve('Microtask execution A').then(console.log);
console.time('Waiting for async');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for async');
Promise.resolve('Microtask execution B').then(console.log);
... if you don't want to go into workers territory and just chunk out this processing out of the main loop (which usually is the best way to handle this).
Finally, some food for thought; this article has a lot of helpful links inside.