I have this function:
const BFS = (graph, start) => {
let queue = []
queue.push(start)
let visited = []
visited[start] = true
while (queue.lenght > 0) {
let node = queue.shift()
for (var i=1; i<graph[node].length; i++) {
if (graph[node][i] && !visited[i]) {
visited[i] = true
queue.push(i)
}
}
}
}
What I want is to have a button that when I press it, It stops the execution of the function, and if I press it again, it resumes the execution from the same place it stopped on.
Is that possible ? and if so, how to do it ?
I have this function:
const BFS = (graph, start) => {
let queue = []
queue.push(start)
let visited = []
visited[start] = true
while (queue.lenght > 0) {
let node = queue.shift()
for (var i=1; i<graph[node].length; i++) {
if (graph[node][i] && !visited[i]) {
visited[i] = true
queue.push(i)
}
}
}
}
What I want is to have a button that when I press it, It stops the execution of the function, and if I press it again, it resumes the execution from the same place it stopped on.
Is that possible ? and if so, how to do it ?
Share Improve this question asked May 30, 2021 at 14:56 Ayman TarigAyman Tarig 751 silver badge7 bronze badges 8-
I don't think there is such a way to do so, other than using breakpoints. I am guessing you are trying to debug the code? If this is the case, make sure that the word
length
is correctly spelled in the linewhile (queue.lenght > 0) {
. – Adnan Commented May 30, 2021 at 15:09 - 1 What you want is not possible. The closest is to attach a click event to the button to run some JS. When you're runnign a JS code, everything else in a browser is blocked, until the JS has been executed, there's no way to detect a button click during the JS execution. – Teemu Commented May 30, 2021 at 15:16
- @Adnan I'm not trying do debug the code .. the thing is this function is a part of visualization process, and I want the user to be able to stop the visualization whenever he wants then resume it again. – Ayman Tarig Commented May 30, 2021 at 15:16
- @MikeM It is OK to make changes to the BFS function, if that change solves the issue – Ayman Tarig Commented May 30, 2021 at 15:20
- @Teemu, it is possible - with generator functions, checkout my solution. – Monsieur Merso Commented May 30, 2021 at 18:58
4 Answers
Reset to default 7Other solution with generator functions, read about this feature on MDN
Note, that here we can proceed further one step at a time!
General idea:
- Put
yield
statements in your method, in places where you want to pause it. - Create instance of your generator and write code that will call its
.next()
method and will handle repeated calls. - Note, that you can get values from generator and also pass in with
.next()
method.
// generator function that we want to stop/continue in the middle
function* stoppableMethod() {
// here is the implementation of the algorithm
// that we want to control
let i = 0;
while (true) {
// note that yield is inside infinite loop!
yield i;
i += 1;
}
}
const generatorInstance = stoppableMethod();
// tick generator and perform update of the indicator
const nextStep = () => {
const { value } = generatorInstance.next();
document.getElementById("indicator").innerHTML = value;
}
// state to keep track of the setInterval id
const state = {
timeoutId: 0,
}
// start method progression
const start = () => {
// do not start interval if there is already an interval
// running
if (state.timeoutId === 0) {
state.timeoutId = setInterval(() => nextStep(), 1000)
}
}
// clear timeout to stop auto porgress
const stop = () => {
clearTimeout(state.timeoutId);
state.timeoutId = 0;
}
// tick further one step
const stepForward = () => nextStep()
<button onclick="start()">start</button>
<button onclick="stop()">pause</button>
<button onclick="nextStep()">one step forward</button>
<div id="indicator">0</div>
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>
You can use this but i am not sure if this helps you
as far as I know other than this you cant stop running function from outside
document.getElementById("start").addEventListener("click", startInterval);
document.getElementById("stop").addEventListener("click", stopInterval);
// You'll need a variable to store a reference to the timer
var timer = null;
function startInterval() {
// Then you initilize the variable
timer = setInterval(function() {
console.log("Foo Executed!");
}, 800);
}
function stopInterval() {
// To cancel an interval, pass the timer to clearInterval()
clearInterval(timer);
}
<button type="button" id="start">Start</button>
<button type="button" id="stop">Stop</button>
Everything is possible. However you have to keep a state. I mean you give a state to your function to start with and it does it things up until you click a button and then it saves the state and stops. Once you click the button and change the state to active again it continues from where it left.
In this particular case your state could be;
var state = { active: false
, graph : myGraph
, node : currentNode
, index : i
}
Since you are basically doing the thing in the while
loop i would add a statement like;
while (queue.lenght > 0 && state.active)
and once somebody clicks the button and toggles state.active
to false
the while
loop, terminates and at that point, i mean right after the while
loop, you save your new state
. So obviously the next time you active this function your code should be able to start from wherever it was left.
Here is a simple mock up to show how it might be implemented;
var state = { active: false
, count : "0"
};
document.querySelector("#b")
.onclick = function(){
let run = n => setTimeout( n => ( this.textContent = n.toString()
.padStart(6,"0")
, state.active && run(++n)
)
, 100
, n
);
state.active ? ( state.active = false
, state.count = this.textContent
)
: ( state.active = true
, run(state.count)
);
};
<button id="b">000000</button>