英文:
Managing cancellation of an asynchronous task
问题
我正在使用Web Speech API 以短暂的延迟读出一个单词数组(为我儿子的拼写测试!)。我已经定义了一个异步函数来朗读单个单词,并使用setTimeout()
来延迟下一个单词5秒。一切都按照要求工作,除非在STOP按钮之后立即按下START按钮,而5秒的超时尚未完成。这将导致整个单词数组重新开始,而来自初始测试的剩余单词会插入其中。我尝试通过取消setTimeout
方法和在超时期间禁用START按钮来修复这个问题,但没有成功。
// 初始化合成器
const synth = window.speechSynthesis;
// 获取UI元素
const startButton = document.querySelector("#start");
let started = false;
const stopButton = document.querySelector("#stop");
stopButton.disabled = true;
// 监听停止按钮
stopButton.addEventListener("click", () => {
startButton.disabled = false;
stopButton.disabled = true;
started = false;
synth.cancel();
});
// 获取语音选项
const voices = synth.getVoices();
const GBvoice = voices.filter((voice) => {
return voice.lang == "en-GB";
});
// 朗读单个单词
async function speakWord(word) {
const utterThis = new SpeechSynthesisUtterance(word);
utterThis.voice = GBvoice[1];
utterThis.pitch = 1;
utterThis.rate = 1;
synth.speak(utterThis);
}
// 定义延迟函数
const addDelay = (t) => {
return new Promise((resolve) => {
setTimeout(resolve.bind(null), t);
});
};
// 定义拼写单词
const words = ["column", "solemn", "autumn", "foreign", "crescent", "spaghetti", "reign", "fascinating", "whistle", "thistle"];
// 问题 - 当在超时期间按下启动按钮时,将朗读两个单词列表
startButton.onclick = async function () {
startButton.disabled = true;
stopButton.disabled = false;
started = true;
for (let word of words) {
await speakWord(word).then(addDelay.bind(null, 5000));
if (!started) {
break;
}
}
};
<button id="stop">停止</button>
<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 -->
// initiate the synth
const synth = window.speechSynthesis;
// grab the UI elements
const startButton = document.querySelector("#start");
let started = false;
const stopButton = document.querySelector("#stop");
stopButton.disabled = true;
// listen to the stop button
stopButton.addEventListener("click", () => {
startButton.disabled = false;
stopButton.disabled = true;
started = false;
synth.cancel();
});
// get the voices
const voices = synth.getVoices();
const GBvoice = voices.filter((voice) => {
return voice.lang == "en-GB";
});
// speak a single word
async function speakWord(word) {
const utterThis = new SpeechSynthesisUtterance(word);
utterThis.voice = GBvoice[1];
utterThis.pitch = 1;
utterThis.rate = 1;
synth.speak(utterThis);
}
// define delay function
const addDelay = (t) => {
return new Promise((resolve) => {
setTimeout(resolve.bind(null), t);
});
};
// define the spelling words
const words = ["column", "solemn", "autumn", "foreign", "crescent", "spaghetti", "reign", "fascinating", "whistle", "thistle"];
// problem - when start button is pressed during timeout, two lists of words are spoken
startButton.onclick = async function () {
startButton.disabled = true;
stopButton.disabled = false;
started = true;
for (let word of words) {
await speakWord(word).then(addDelay.bind(null, 5000));
if (!started) {
break;
}
}
};
<!-- language: lang-html -->
<button id="stop">Stop</button>
<button id="start">Start</button>
<!-- end snippet -->
答案1
得分: 3
这是使用 AbortController
API 完成的取消路线的示例代码。为了清晰展示(因为我的浏览器中没有工作的语音合成),已经移除了与语音合成相关的部分,并用简单的 console.log
进行了替换,但你可以轻松地将它们重新加入。
let abc = null;
let jobCounter = 0;
const sleep = ms => new Promise(ok => setTimeout(ok, ms));
document.getElementById('btn.play').addEventListener('click', async ev => {
const job = ++jobCounter;
abc?.abort();
const myAbc = abc = new AbortController();
for (let i = 1; i <= 5; ++i) {
if (myAbc.signal.aborted)
break;
// await speakWord(...)
console.log(`job #${job} says: ${i}`);
await sleep(1000);
}
});
document.getElementById('btn.stop').addEventListener('click', ev => {
abc?.abort();
});
这段代码的核心思想是:不使用简单的标志变量,而是让每个“语音”任务创建一个新的对象来管理其自己的取消状态,然后将该对象放入一个全局变量中,以便任何按钮的处理程序可以找到它。任何按钮点击时,如果存在当前任务,它将取消当前任务,然后播放按钮将启动一个新任务。这种方式可以避免原始方法中的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 -->
let abc = null;
let jobCounter = 0;
const sleep = ms => new Promise(ok => setTimeout(ok, ms));
document.getElementById('btn.play').addEventListener('click', async ev => {
const job = ++jobCounter;
abc?.abort();
const myAbc = abc = new AbortController();
for (let i = 1; i <= 5; ++i) {
if (myAbc.signal.aborted)
break;
// await speakWord(...)
console.log(`job #${job} says: ${i}`);
await sleep(1000);
}
});
document.getElementById('btn.stop').addEventListener('click', ev => {
abc?.abort();
});
<!-- language: lang-html -->
<button id="btn.play">PLAY</button>
<button id="btn.stop">STOP</button>
<!-- 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论