To pause and resume the execution of a function at a particular place at the click of a button, we can use either yield within a generator function or await a Promise within an async function.
Either way, we need to yield to or await an asynchronous function like setTimeout, setInterval or requestIdleCallback to give the single thread of JavaScript execution the opportunity to execute any event-handler callbacks that can then control when the paused function is resumed.
See the JavaScript event loop to understand this further.
Suppose we have a button and a function f that we want to be able to pause at the line shown.
function f() {
let i = 0;
while (true) {
// pause here when button clicked
button.textContent = ++i;
}
}
If using yield then f could be amended to
function* f() {
let i = 0;
while (true) {
yield;
button.textContent = ++i;
}
}
We would then create an iterator from this generator function and use iterator.next() in an asynchronously executed callblack to resume execution.
If using await then f could instead be amended to
async function f() {
let i = 0;
while (true) {
await new Promise(executor);
button.textContent = ++i;
}
}
where executor is a function which calls an asynchronous function that resolves the executor to resume execution from the await.
Some examples:
Using a generator function and setTimeout.
const button = document.querySelector('button');
let started = false;
let iterator = f();
const nextTick = () => {
if (started) {
iterator.next();
setTimeout(nextTick);
}
};
button.addEventListener('click', () => {
started = !started;
nextTick();
});
function* f() {
let i = 0;
while (true) {
yield;
button.textContent = ++i;
}
}
button {
text-align: center; padding: .5rem; width: 16rem;
font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
}
<button>Click me</button>
Using async and await:
const button = document.querySelector('button');
let started = false;
let resolve;
const nextTick = () => new Promise(res => {
resolve = res;
setTimeout(() => {
if (started) resolve();
});
});
button.addEventListener('click', () => {
started = !started;
if (started) resolve();
});
async function f() {
let i = 0;
while (true) {
await nextTick();
button.textContent = ++i;
}
}
f();
button {
text-align: center; padding: .5rem; width: 16rem;
font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
}
<button>Click me</button>
Using setInterval, which is basically the same as using setTimeout but less flexible:
const button = document.querySelector('button');
let id = 0;
let started = false;
const iterator = f();
const next = iterator.next.bind(iterator);
button.addEventListener('click', () => {
started = !started;
if (started) {
if (id === 0) id = setInterval(next, 0);
} else if (id !== 0) {
clearInterval(id);
id = 0;
}
});
function* f() {
let i = 0;
while (true) {
yield;
button.textContent = ++i;
}
}
button {
text-align: center; padding: .5rem; width: 16rem;
font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
}
<button>Click me</button>
The problem with using asynchronous functions like setTimeout or setInterval here is that they are subject to a minimum delay of several milliseconds, which can be seen in how slow the number increments in the examples above.
Notice how much faster the number increments using a MessageChannel to fire the button-enabling asynchronous callback.
const button = document.querySelector('button');
let nextTick;
button.addEventListener('click', (() => {
let started = false;
let resolve;
const { port1, port2 } = new MessageChannel();
port2.onmessage = () => {
if (started) resolve();
};
nextTick = () => new Promise(res => {
resolve = res;
port1.postMessage(null);
});
return () => {
started = !started;
if (started) resolve();
};
})());
async function f() {
let i = 0;
while (true) {
await nextTick();
button.textContent = ++i;
}
}
f();
button {
text-align: center; padding: .5rem; width: 16rem;
font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
}
<button>Click me</button>
Note that in these examples nextTick could have been named anything, it is not the same as the process.nextTick of Node.js. See setImmediate if using Node.js.
If you are animating you may be using requestAnimationFrame and that also executes its callback asynchronously, so it can be used here.
The following shows the relative speed of a variety of asynchronous functions:
const button = document.querySelector('button');
button.addEventListener('click', (() => {
const setImmediateAnalogues = [
setTimeout,
requestAnimationFrame,
cb => requestIdleCallback(cb, { timeout: 0 }),
cb => {
window.onmessage = event => {
// event.origin should be validated here
if (event.source === window) cb();
};
window.postMessage('', window.location);
},
(() => {
const { port1, port2 } = new MessageChannel();
return cb => {
port2.onmessage = cb;
port1.postMessage('');
};
})(),
];
let setImmediate = setTimeout;
for (const rb of document.querySelectorAll('input[name="rb"]')) {
const analog = setImmediateAnalogues.shift();
rb.addEventListener('click', () => setImmediate = analog);
}
const iterator = f();
let started = false;
const nextTick = () => {
if (started) {
iterator.next();
setImmediate(nextTick);
}
};
return () => {
started = !started;
nextTick();
};
})());
function* f() {
let i = 0;
while (true) {
yield;
button.textContent = ++i;
}
}
button { text-align: center; padding: .5rem; width: 16rem;
font-size: 2rem; border-radius: .5rem; margin-left: .25rem; }
label { padding: .1rem; display: block; font-family: monospace; }
<label><input type='radio' name='rb' value='' checked> setTimeout</label>
<label><input type='radio' name='rb' value=''> requestAnimationFrame</label>
<label><input type='radio' name='rb' value=''> requestIdleCallback</label>
<label><input type='radio' name='rb' value=''> window.postMessage</label>
<label><input type='radio' name='rb' value=''> MessageChannel</label>
<br>
<button>Click me</button>
lengthis correctly spelled in the linewhile (queue.lenght > 0) {.