如何在JavaScript中暂停和恢复函数执行

5

我有这个函数:

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)
      }
    }
  }
}

我想要一个按钮,当我按下它时,可以停止函数的执行,如果我再次按下它,函数会从停止的地方继续执行。

这种操作是否可行?如果是,怎样实现?


我认为除了使用断点之外,没有其他方法可以这样做。我猜你正在尝试调试代码?如果是这种情况,请确保在while (queue.lenght > 0) {行中正确拼写单词“length”。 - Adnan
1
你想要的是不可能实现的。最接近的方法是将单击事件附加到按钮,以运行一些JS代码。当您运行JS代码时,浏览器中的其他所有内容都被阻止,直到JS被执行,无法在JS执行期间检测到按钮点击。 - Teemu
@Teemu,这是可能的——使用生成器函数,看看我的解决方案。 - Monsieur Merso
@Teemu,现在我明白你的意思了,但我认为,由于setInterval仅会阻塞一个算法步骤所需的时间(这取决于yield关键字的位置),因此对于具有相对快速步骤的算法来说,这种阻塞是可以忽略不计的。 - Monsieur Merso
添加了更全面的答案。 - MikeM
显示剩余3条评论
4个回答

6

使用生成函数的其他解决方案,了解此功能请访问 MDN

注意,在这里我们可以一步步地继续进行!

总体思路:

  1. 在您的方法中放置 yield 语句,以表示想要暂停的位置。
  2. 创建您的生成器实例,并编写调用其 .next() 方法和处理重复调用的代码。
  3. 请注意,您可以通过 .next() 方法从生成器中获取值并且传入值。

// 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>


1
如果您连续点击“开始”按钮两次或更多次,则会激活您的计数器覆盖之前的内容。除此之外,使用生成器是一个好主意。 - Redu

3
为了在单击按钮时暂停和恢复函数的执行,我们可以在生成器函数中使用yield,或在async函数awaitPromise。无论哪种方式,我们都需要向异步函数(如setTimeoutsetIntervalrequestIdleCallback)发出yieldawait,以便让JavaScript执行线程有机会执行任何事件处理程序回调,从而控制暂停函数何时恢复。请参见JavaScript event loop以进一步了解此内容。假设我们有一个按钮和一个名为f的函数,我们希望能够在所示行处暂停。
function f() {
  let i = 0;
  while (true) {
    // pause here when button clicked
    button.textContent = ++i;
  }
}

如果使用yield,那么f可以修改为:
function* f() {
  let i = 0;
  while (true) {
    yield;
    button.textContent = ++i;
  }
}

我们将从这个生成器函数创建一个迭代器,并在异步执行的回调中使用iterator.next()来恢复执行。
如果使用await,那么f可以改为:
async function f() {
  let i = 0;
  while (true) {
    await new Promise(executor);
    button.textContent = ++i;
  }
}

其中executor是一个调用异步函数的函数,该函数解析执行器以从await中恢复执行。


一些例子:

使用生成器函数和 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>

使用asyncawait:

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>

使用 setInterval,它基本上与使用 setTimeout 相同,但不够灵活。

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>


使用像setTimeoutsetInterval这样的异步函数存在一个问题,即它们受到几毫秒的最小延迟的影响,这可以从上面示例中数字增加的缓慢程度中看出。
请注意,使用MessageChannel来触发按钮启用异步回调会使数字增加速度更快。

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>

请注意,在这些示例中,nextTick 可以被命名为任何名称,它与 Node.js 的 process.nextTick 不同。如果使用 Node.js,请参见 setImmediate
如果正在进行动画,则可能正在使用 requestAnimationFrame,它也会异步执行其回调函数,因此可以在此处使用。
以下显示了各种异步函数的相对速度:

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>


1
你可以尝试使用这个,但我不确定它是否有帮助。
据我所知,除此之外,你无法阻止外部运行函数。

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>


0

一切皆有可能。但是你必须保持状态。我的意思是,你给函数一个状态来开始它的工作,直到你点击一个按钮,然后它保存状态并停止。一旦你点击按钮并将状态重新设置为活动状态,它就会从离开的地方继续。

在这种特殊情况下,你的状态可以是:

var state = { active: false
            , graph : myGraph
            , node  : currentNode
            , index : i
            }

由于你基本上是在while循环中执行该操作,我建议添加一个语句,例如:

while (queue.lenght > 0 && state.active)

一旦有人点击按钮并将state.active切换为falsewhile循环就会终止,并且在此时,在while循环之后,您保存了新的state。 所以显然,下次激活此功能时,您的代码应该能够从上次离开的地方开始。

以下是一个简单的模拟展示它可能如何实现;

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>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接