我们面临的问题是在React函数组件中,当我们通过useState
更新了一个状态(这里是audioUrl
)后,我们希望立刻获取到最新的值来播放语音。但useState
的更新是异步的,在更新后立即访问状态变量并不能获取到最新值。本文将介绍三种尝试方式(其中两种失败,一种成功)以及最终的解决方案。
问题背景
在实现一个聊愈功能时,我们需要获取输出的语音链接并进行播放。我们使用audioUrl
状态来存储语音链接,并通过handleAudioToggle
方法进行播放。每次获取到新的语音链接后,我们会更新audioUrl
状态,然后调用handleAudioToggle
来播放。然而,我们发现更新状态后立即调用播放函数时,audioUrl
的值尚未更新,导致播放失败(例如404错误,因为新链接可能还未准备好)。
因为在react合成事件中改变状态是异步的,出于减少render次数,react会收集所有状态变更,然后比对优化,最后做一次变更;
尝试一:直接赋值(将newUrl直接传递给播放组件)
我们尝试在设置状态的同时,直接将新的URL传递给播放函数,避免依赖状态的当前值。这样我们就能确保传递给播放函数的是最新的URL。
const [audioUrl, setAudioUrl] = useState(''); \\\\不使用useState
const fetchNewAudio = () => {
const newUrl = replaceDomain(
textChat.data.data.text,
"pub-a3b************b2287.r2.dev"
);
handleAudioToggle(newUrl); //直接将url传递给播放函数
};
这个方法在传递URL时是即时的,所以播放函数使用的是最新的URL。但是,由于我们获取到新URL时,语音文件可能还未上传完成或生成,此时调用播放会导致404错误。所以,虽然状态更新和播放函数调用时URL是最新的,但资源未准备好,播放仍会失败。
尝试二:使用useEffect监听状态变化(无效)
我们首先想到使用useEffect
来监听audioUrl
的变化。当audioUrl
变化时,在useEffect
中调用播放函数。这样应该能够确保在状态更新后播放。
const [audioUrl, setAudioUrl] = useState('');
useEffect(() => {
if (audioUrl) {
handleAudioToggle(audioUrl); // 在audioUrl变化后调用播放
}
}, [audioUrl]);
// 在获取到新语音链接的地方
const fetchNewAudio = () => {
const newUrl = replaceDomain(
textChat.data.data.text,
"pub-a3b9222a444c40648c0a11b32ecb2287.r2.dev"
);
setAudioUrl(newUrl); // 设置新的语音链接
};
实际上,我们发现这个方法是无效的。因为当audioUrl
被设置后,useEffect
确实会在状态更新后触发,但是此时新的语音链接可能还未在服务器上生成(即链接设置好,但资源还没有准备好),此时立即播放会导致404。注意:这个尝试并不是为了解决异步更新值的问题(实际上它解决了状态异步更新的问题),而是因为资源未准备好而无法播放。
尝试三:使用Promise延迟播放(有效)
为了解决资源未准备好就播放的问题,我们尝试在设置新URL后延迟一段时间再播放。这样能够给服务器足够的时间生成语音文件并存储。
// 创建一个延迟函数
function delay(timeout) {
return new Promise(resolve => {
setTimeout(() => {
resolve(); // 3秒后解析Promise
}, timeout);
});
}
const fetchNewAudio = async () => {
const newUrl = getNewAudioUrl(); // 获取新的语音链接
setAudioUrl(newUrl); // 更新状态
// 延迟5秒播放
delay(5000)
.then(() => {
handleAudioToggle(newUrl);
console.log("成功触发音频切换");
})
.catch(error => {
console.error("延迟执行失败:", error);
});
};
延迟播放给了服务器足够的时间去生成语音文件。在这段时间内,语音文件可能已经上传完成并可以通过URL访问,所以当之后调用播放时,资源已经准备就绪,播放成功。经过测试,3秒时间一半语音文件能准备就绪,5秒几乎所有文件均能准备就绪
总结
- 问题核心:
useState
的更新是异步的,我们无法在设置状态后立即获取最新值。而且,即使我们能够获取到最新值,由于服务器生成语音文件需要时间,立即播放也会导致404。 - 解决方案:我们通过延迟播放的方式给服务器预留处理时间。同时,我们可以通过直接使用获取到的新URL(而不是依赖状态的最新值)来播放,避免状态异步更新的问题。
- 更优的实践:在需要播放新语音的地方,我们不应该依赖状态的最新值,而是直接使用我们刚刚获得的新URL。因为:
- 状态更新是异步的,在同一个函数作用域内不会立即改变。
- 我们恰好需要延迟播放,所以我们可以将新URL保存在一个变量中(或者使用ref保存),然后在延迟后使用它来播放,这样既避免了状态异步更新的问题,又确保了播放时使用正确的URL。