取消管理异步任务

huangapple go评论82阅读模式
英文:

Managing cancellation of an asynchronous task

问题

我正在使用Web Speech API 以短暂的延迟读出一个单词数组(为我儿子的拼写测试!)。我已经定义了一个异步函数来朗读单个单词,并使用setTimeout()来延迟下一个单词5秒。一切都按照要求工作,除非在STOP按钮之后立即按下START按钮,而5秒的超时尚未完成。这将导致整个单词数组重新开始,而来自初始测试的剩余单词会插入其中。我尝试通过取消setTimeout方法和在超时期间禁用START按钮来修复这个问题,但没有成功。

  1. // 初始化合成器
  2. const synth = window.speechSynthesis;
  3. // 获取UI元素
  4. const startButton = document.querySelector("#start");
  5. let started = false;
  6. const stopButton = document.querySelector("#stop");
  7. stopButton.disabled = true;
  8. // 监听停止按钮
  9. stopButton.addEventListener("click", () => {
  10. startButton.disabled = false;
  11. stopButton.disabled = true;
  12. started = false;
  13. synth.cancel();
  14. });
  15. // 获取语音选项
  16. const voices = synth.getVoices();
  17. const GBvoice = voices.filter((voice) => {
  18. return voice.lang == "en-GB";
  19. });
  20. // 朗读单个单词
  21. async function speakWord(word) {
  22. const utterThis = new SpeechSynthesisUtterance(word);
  23. utterThis.voice = GBvoice[1];
  24. utterThis.pitch = 1;
  25. utterThis.rate = 1;
  26. synth.speak(utterThis);
  27. }
  28. // 定义延迟函数
  29. const addDelay = (t) => {
  30. return new Promise((resolve) => {
  31. setTimeout(resolve.bind(null), t);
  32. });
  33. };
  34. // 定义拼写单词
  35. const words = ["column", "solemn", "autumn", "foreign", "crescent", "spaghetti", "reign", "fascinating", "whistle", "thistle"];
  36. // 问题 - 当在超时期间按下启动按钮时,将朗读两个单词列表
  37. startButton.onclick = async function () {
  38. startButton.disabled = true;
  39. stopButton.disabled = false;
  40. started = true;
  41. for (let word of words) {
  42. await speakWord(word).then(addDelay.bind(null, 5000));
  43. if (!started) {
  44. break;
  45. }
  46. }
  47. };
  1. <button id="stop">停止</button>
  2. <button id="start">启动</button>
英文:

I am using the Web Speech API to read out an array of words with a short delay between each one (a spelling test for my son!). I have defined an async function to speak a single word and used setTimeout() to delay the following word by 5 seconds. Everything is working as required, except when the START button is pressed immediately after the STOP button, before the 5 second timeout has resolved. This results in the whole array of words starting again, with the remaining words from the initial test threaded in between. I have tried to fix this by cancelling the setTimeout method and by disabling the START button while the timeout is active, but without success.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

  1. // initiate the synth
  2. const synth = window.speechSynthesis;
  3. // grab the UI elements
  4. const startButton = document.querySelector(&quot;#start&quot;);
  5. let started = false;
  6. const stopButton = document.querySelector(&quot;#stop&quot;);
  7. stopButton.disabled = true;
  8. // listen to the stop button
  9. stopButton.addEventListener(&quot;click&quot;, () =&gt; {
  10. startButton.disabled = false;
  11. stopButton.disabled = true;
  12. started = false;
  13. synth.cancel();
  14. });
  15. // get the voices
  16. const voices = synth.getVoices();
  17. const GBvoice = voices.filter((voice) =&gt; {
  18. return voice.lang == &quot;en-GB&quot;;
  19. });
  20. // speak a single word
  21. async function speakWord(word) {
  22. const utterThis = new SpeechSynthesisUtterance(word);
  23. utterThis.voice = GBvoice[1];
  24. utterThis.pitch = 1;
  25. utterThis.rate = 1;
  26. synth.speak(utterThis);
  27. }
  28. // define delay function
  29. const addDelay = (t) =&gt; {
  30. return new Promise((resolve) =&gt; {
  31. setTimeout(resolve.bind(null), t);
  32. });
  33. };
  34. // define the spelling words
  35. const words = [&quot;column&quot;, &quot;solemn&quot;, &quot;autumn&quot;, &quot;foreign&quot;, &quot;crescent&quot;, &quot;spaghetti&quot;, &quot;reign&quot;, &quot;fascinating&quot;, &quot;whistle&quot;, &quot;thistle&quot;];
  36. // problem - when start button is pressed during timeout, two lists of words are spoken
  37. startButton.onclick = async function () {
  38. startButton.disabled = true;
  39. stopButton.disabled = false;
  40. started = true;
  41. for (let word of words) {
  42. await speakWord(word).then(addDelay.bind(null, 5000));
  43. if (!started) {
  44. break;
  45. }
  46. }
  47. };

<!-- language: lang-html -->

  1. &lt;button id=&quot;stop&quot;&gt;Stop&lt;/button&gt;
  2. &lt;button id=&quot;start&quot;&gt;Start&lt;/button&gt;

<!-- end snippet -->

答案1

得分: 3

这是使用 AbortController API 完成的取消路线的示例代码。为了清晰展示(因为我的浏览器中没有工作的语音合成),已经移除了与语音合成相关的部分,并用简单的 console.log 进行了替换,但你可以轻松地将它们重新加入。

  1. let abc = null;
  2. let jobCounter = 0;
  3. const sleep = ms => new Promise(ok => setTimeout(ok, ms));
  4. document.getElementById('btn.play').addEventListener('click', async ev => {
  5. const job = ++jobCounter;
  6. abc?.abort();
  7. const myAbc = abc = new AbortController();
  8. for (let i = 1; i <= 5; ++i) {
  9. if (myAbc.signal.aborted)
  10. break;
  11. // await speakWord(...)
  12. console.log(`job #${job} says: ${i}`);
  13. await sleep(1000);
  14. }
  15. });
  16. document.getElementById('btn.stop').addEventListener('click', ev => {
  17. abc?.abort();
  18. });

这段代码的核心思想是:不使用简单的标志变量,而是让每个“语音”任务创建一个新的对象来管理其自己的取消状态,然后将该对象放入一个全局变量中,以便任何按钮的处理程序可以找到它。任何按钮点击时,如果存在当前任务,它将取消当前任务,然后播放按钮将启动一个新任务。这种方式可以避免原始方法中的ABA问题。

在这个示例中,你可能可以使用更简单的方式来替代 AbortController(比如一个带有 cancelled 属性的普通对象),但在一般情况下,如果你需要调用其他 Web API(比如 fetch),可能仍需要准备一个 AbortController

英文:

It may seem a little over-engineered, but here’s the cancellation route done using the AbortController API.

For clarity of presentation (and because I have no working speech synthesis in my browser), all parts related to speech synthesis were removed and replaced with a bare console.log, but you should be able to put them back easily.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

  1. let abc = null;
  2. let jobCounter = 0;
  3. const sleep = ms =&gt; new Promise(ok =&gt; setTimeout(ok, ms));
  4. document.getElementById(&#39;btn.play&#39;).addEventListener(&#39;click&#39;, async ev =&gt; {
  5. const job = ++jobCounter;
  6. abc?.abort();
  7. const myAbc = abc = new AbortController();
  8. for (let i = 1; i &lt;= 5; ++i) {
  9. if (myAbc.signal.aborted)
  10. break;
  11. // await speakWord(...)
  12. console.log(`job #${job} says: ${i}`);
  13. await sleep(1000);
  14. }
  15. });
  16. document.getElementById(&#39;btn.stop&#39;).addEventListener(&#39;click&#39;, ev =&gt; {
  17. abc?.abort();
  18. });

<!-- language: lang-html -->

  1. &lt;button id=&quot;btn.play&quot;&gt;PLAY&lt;/button&gt;
  2. &lt;button id=&quot;btn.stop&quot;&gt;STOP&lt;/button&gt;

<!-- end snippet -->

In essence: instead of using a bare flag variable, each ‘speech’ job creates a new object managing its own cancellation state, then puts that object in a global variable where it can be found by either button’s handler. Either button, when clicked, will cancel the current job, if there is one; the play button will then start a new job. This way, the ABA problem of the original approach is averted.

In this example you may be able to get away with replacing AbortController with something simpler (like a plain object with a cancelled property), but in the general case where you need to invoke other Web APIs (like fetch), you may need to have an AbortController at the ready after all.

huangapple
  • 本文由 发表于 2023年2月27日 04:26:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/75574827.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定