JavaScript怎么模擬 delay、sleep、pause、wait 方法
許多編程語(yǔ)言都有一個(gè) sleep 函數(shù),可以延遲程序的執(zhí)行若干秒。JavaScript缺少這個(gè)內(nèi)置功能,但不用擔(dān)心。在這篇文章中,我們將探討在JavaScript代碼中實(shí)現(xiàn)延遲的各種技巧,同時(shí)考慮到該語(yǔ)言的異步性質(zhì)。
如何在 JS 中創(chuàng)建 sleep 函數(shù)
對(duì)于那些只想快速解決問(wèn)題而不想深入了解技術(shù)細(xì)節(jié)的人,我們也有簡(jiǎn)單明了的解決方案。下面是如何在你的JavaScript工具箱中添加一個(gè) sleep 函數(shù)的最直接方式:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
console.log('Hello');
sleep(2000).then(() => { console.log('World!'); });
運(yùn)行這段代碼,你會(huì)在控制臺(tái)看到 “Hello”。然后,在短暫的兩秒鐘后,“World!”v會(huì)接著出現(xiàn)。這是一種既簡(jiǎn)潔又有效的引入延遲的方法。
如果你只是為了這個(gè)來(lái)的,那太好了!但如果你對(duì)“為什么”和“怎么做”的原因感到好奇,還有更多可以學(xué)習(xí)的內(nèi)容。JavaScript中處理時(shí)間有其細(xì)微之處,了解這些可能會(huì)對(duì)你有所幫助。
理解JavaScript的執(zhí)行模型
現(xiàn)在我們已經(jīng)有了一個(gè)快速的解決方案,讓我們深入了解JavaScript的執(zhí)行模型的機(jī)制。理解這一點(diǎn)對(duì)于有效地管理代碼中的時(shí)間和異步操作至關(guān)重要。
考慮以下Ruby代碼:
require 'net/http'
require 'json'
url = 'https://api.github.com/users/jameshibbard'
uri = URI(url)
response = JSON.parse(Net::HTTP.get(uri))
puts response['public_repos']
puts 'Hello!'
正如人們所期望的,這段代碼向GitHub API發(fā)送一個(gè)請(qǐng)求以獲取我的用戶數(shù)據(jù)。然后解析響應(yīng),輸出與我的GitHub帳戶關(guān)聯(lián)的公共倉(cāng)庫(kù)的數(shù)量,最后在屏幕上打印“Hello!”。執(zhí)行是從上到下進(jìn)行的。
相比之下,這是相同功能的JavaScript版本:
fetch('https://api.github.com/users/jameshibbard')
.then(res => res.json())
.then(json => console.log(json.public_repos));
console.log('Hello!');
如果你運(yùn)行這段代碼,它會(huì)先在屏幕上輸出“Hello!”,然后輸出與我的GitHub帳戶關(guān)聯(lián)的公共倉(cāng)庫(kù)的數(shù)量。
這是因?yàn)樵贘avaScript中,從API獲取數(shù)據(jù)是一個(gè)異步操作。JavaScript解釋器會(huì)遇到 fetch 命令并發(fā)送請(qǐng)求。然而,它不會(huì)等待請(qǐng)求完成。相反,它會(huì)繼續(xù)執(zhí)行,將“Hello!”輸出到控制臺(tái),然后當(dāng)請(qǐng)求在幾百毫秒后返回時(shí),它會(huì)輸出倉(cāng)庫(kù)的數(shù)量。
如何在JavaScript中正確使用SetTimeout
既然我們已經(jīng)更好地理解了JavaScript的執(zhí)行模型,讓我們看看JavaScript是如何處理延遲和異步代碼的。
在JavaScript中創(chuàng)建延遲的標(biāo)準(zhǔn)方法是使用其 setTimeout 方法。例如:
console.log('Hello');
setTimeout(() => { console.log('World!'); }, 2000);
這將在控制臺(tái)上輸出 "Hello",然后兩秒后輸出 "World!"。在很多情況下,這已經(jīng)足夠了:做某事,然后在短暫的延遲后,做其他事情。問(wèn)題解決!
但不幸的是,事情并不總是那么簡(jiǎn)單。
你可能會(huì)認(rèn)為 setTimeout 會(huì)暫停整個(gè)程序,但事實(shí)并非如此。它是一個(gè)異步函數(shù),這意味著其余的代碼不會(huì)等待它完成。
例如,假設(shè)你運(yùn)行了以下代碼:
console.log('Hello');
setTimeout(() => { console.log('World!'); }, 2000);
console.log('Goodbye!');
你會(huì)看到以下輸出:
Hello
Goodbye!
World!
注意“Goodbye!”是如何出現(xiàn)在“World!”之前的?這是因?yàn)?nbsp;setTimeout 不會(huì)阻塞其余代碼的執(zhí)行。
這意味著你不能這樣做:
console.log('Hello');
setTimeout(1000);
console.log('World');
"Hello" 和 "World" 會(huì)立即被記錄在控制臺(tái)上,之間沒(méi)有明顯的延遲。
你也不能這樣做:
for (let i = 0; i < 5; i++) {
setTimeout(() => { console.log(i); }, i * 1000);
}
花一秒鐘考慮一下上面的代碼片段可能會(huì)發(fā)生什么。
它不會(huì)在每個(gè)數(shù)字之間延遲一秒鐘打印數(shù)字 0 到 4。相反,你實(shí)際上會(huì)得到五個(gè) 4,它們?cè)谒拿牒笠淮涡匀看蛴〕鰜?lái)。為什么呢?因?yàn)檠h(huán)不會(huì)暫停執(zhí)行。它不會(huì)等待 setTimeout 完成才進(jìn)入下一次迭代。
那么 setTimeout 實(shí)際上有什么用呢?現(xiàn)在讓我們來(lái)看看。
setTimeout() 函數(shù)的檢查和最佳實(shí)踐
正如你可以在我們的 setTimeout 教程中閱讀到的,原生JavaScript setTimeout 函數(shù)在指定的延遲(以毫秒為單位)后調(diào)用一個(gè)函數(shù)或執(zhí)行一個(gè)代碼片段。
這可能在某些情況下是有用的,例如,如果你希望在訪問(wèn)者瀏覽你的頁(yè)面一段時(shí)間后顯示一個(gè)彈出窗口,或者你希望在從元素上移除懸停效果之前有短暫的延遲(以防用戶意外地鼠標(biāo)移出)。
setTimeout 方法接受一個(gè)函數(shù)的引用作為第一個(gè)參數(shù)。
這可以是函數(shù)的名稱:
function greet(){
alert('Howdy!');
}
setTimeout(greet, 2000);
它可以是一個(gè)指向函數(shù)的變量(函數(shù)表達(dá)式):
const greet = function(){
alert('Howdy!');
};
setTimeout(greet, 2000);
或者它可以是一個(gè)匿名函數(shù)(在這種情況下是箭頭函數(shù)):
setTimeout(() => { alert('Howdy!'); }, 2000);
也可以將一段代碼字符串傳遞給 setTimeout 以供其執(zhí)行:
然而,這種方法是不可取的,因?yàn)椋?/p>
- 它很難閱讀(因此很難維護(hù)和/或調(diào)試)
- 它使用了一個(gè)隱含的 eval,這是一個(gè)潛在的安全風(fēng)險(xiǎn)
- 它比替代方案慢,因?yàn)樗仨氄{(diào)用JS解釋器
如前所述,setTimeout 非常適合在延遲后觸發(fā)一次性操作,但也可以使用 setTimeout(或其表親 setInterval)來(lái)讓JavaScript等待直到滿足某個(gè)條件。例如,下面是如何使用 setTimeout 等待某個(gè)元素出現(xiàn)在網(wǎng)頁(yè)上的方式:
function pollDOM () {
const el = document.querySelector('my-element');
if (el.length) {
// Do something with el
} else {
setTimeout(pollDOM, 300); // try again in 300 milliseconds
}
}
pollDOM();
這假設(shè)該元素最終會(huì)出現(xiàn)。如果你不確定這是否會(huì)發(fā)生,你需要考慮取消計(jì)時(shí)器(使用 clearTimeout 或 clearInterval)。
在 JS 中使用遞增超時(shí)作為 Sleep 函數(shù)的替代方案
有時(shí),你可能會(huì)發(fā)現(xiàn)自己想要在一系列操作中引入延遲。雖然你可以使用各種方法來(lái)模擬一個(gè)Sleep函數(shù),但還有另一種經(jīng)常被忽視的方法:遞增超時(shí)。
這個(gè)思路很簡(jiǎn)單:你不是暫停整個(gè)執(zhí)行線程,而是使用 setTimeout 為每個(gè)后續(xù)操作增加延遲。這樣,你可以創(chuàng)建一個(gè)延遲操作的序列,而不會(huì)阻塞瀏覽器或損害用戶體驗(yàn)。
下面是一個(gè)快速示例:
let delay = 1000; // 從1秒的延遲開(kāi)始
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(`這是消息 ${i + 1}`);
}, delay);
delay += 1000; // 每次迭代延遲增加1秒
}
在這個(gè)示例中,第一條消息將在1秒后出現(xiàn),第二條消息在2秒后,依此類推,直到第五條消息在5秒后。
這種方法的優(yōu)點(diǎn)是它不阻塞,易于實(shí)現(xiàn),并且不需要了解 promises 或 async/await。然而,它不適用于需要精確計(jì)時(shí)或錯(cuò)誤處理的復(fù)雜異步操作
現(xiàn)代JavaScript中的流控制
編寫 JavaScript 時(shí),我們經(jīng)常需要等待某件事情發(fā)生(例如,從 API 獲取數(shù)據(jù)),然后做出響應(yīng)(例如,更新UI以顯示數(shù)據(jù))。
上面的示例使用了一個(gè)匿名回調(diào)函數(shù)來(lái)實(shí)現(xiàn)這一目的,但如果你需要等待多個(gè)事情發(fā)生,語(yǔ)法很快就會(huì)變得相當(dāng)復(fù)雜,你最終會(huì)陷入回調(diào)地獄。
幸運(yùn)的是,這門語(yǔ)言在過(guò)去幾年里有了很大的發(fā)展,現(xiàn)在為我們提供了新的構(gòu)造來(lái)避免這一點(diǎn)。
例如,使用 async await,我們可以重寫最初獲取 GitHub API信息的代碼:
(async () => {
const res = await fetch(`https://api.github.com/users/jameshibbard`);
const json = await res.json();
console.log(json.public_repos);
console.log('Hello!');
})();
現(xiàn)在,代碼從上到下執(zhí)行。JavaScript 解釋器等待網(wǎng)絡(luò)請(qǐng)求完成,首先記錄公共倉(cāng)庫(kù)的數(shù)量,然后記錄“Hello!”消息。
將Sleep函數(shù)引入原生JavaScript
如果你還在看這篇文章,那么我猜你一定是想阻塞那個(gè)執(zhí)行線程,并讓JavaScript等待一下。
下面是你可能會(huì)這樣做的方式:
function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
console.log('Hello');
sleep(2000);
console.log('World!');
正如預(yù)期的那樣,這將在控制臺(tái)上打印“Hello”,暫停兩秒,然后打印“World!”
它通過(guò)使用Date.now方法獲取自1970年1月1日以來(lái)經(jīng)過(guò)的毫秒數(shù),并將該值分配給一個(gè) date 變量。然后它創(chuàng)建一個(gè)空的 currentDate 變量,然后進(jìn)入一個(gè) do ... while 循環(huán)。在循環(huán)中,它會(huì)重復(fù)獲取自1970年1月1日以來(lái)經(jīng)過(guò)的毫秒數(shù),并將該值分配給之前聲明的 currentDate 變量。只要 date 和 currentDate 之間的差異小于所需的毫秒數(shù)的延遲,循環(huán)就會(huì)繼續(xù)進(jìn)行。
任務(wù)完成了,對(duì)嗎?好吧,也不完全是……
如何在JavaScript中編寫更好的Sleep函數(shù)
也許這段代碼正是你所期望的,但請(qǐng)注意,它有一個(gè)很大的缺點(diǎn):循環(huán)會(huì)阻塞JavaScript的執(zhí)行線程,并確保在它完成之前沒(méi)有人能與你的程序進(jìn)行交互。如果你需要很大的延遲,甚至有可能會(huì)讓整個(gè)程序崩潰。
那么應(yīng)該怎么做呢?
事實(shí)上,也可以結(jié)合本文前面學(xué)到的技巧來(lái)制作一個(gè)不太侵入性的 sleep 方法:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
console.log('Hello');
sleep(2000).then(() => { console.log('World!'); });
這段代碼將在控制臺(tái)上打印“Hello”,等待兩秒,然后打印“World!”在底層,我們使用setTimeout 方法在給定的毫秒數(shù)后解析一個(gè) promise。
注意,我們需要使用一個(gè) then 回調(diào)來(lái)確保第二條消息是帶有延遲的。我們還可以在第一個(gè)回調(diào)函數(shù)后面鏈?zhǔn)降靥砑痈嗷卣{(diào)函數(shù)。
這樣做是可行的,但看起來(lái)不太好看。我們可以使用async ... await來(lái)美化它:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedGreeting() {
console.log('Hello');
await sleep(2000);
console.log('World!');
await sleep(2000);
console.log('Goodbye!');
}
delayedGreeting();
這看起來(lái)更好看,但這意味著使用 sleep 函數(shù)的任何代碼都需要被標(biāo)記為 async。
當(dāng)然,這兩種方法仍然有一個(gè)缺點(diǎn)(或特點(diǎn)),那就是它們不會(huì)暫停整個(gè)程序的執(zhí)行。只有你的函數(shù)會(huì)睡眠:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedGreeting() {
console.log('Hello');
await sleep(2000); // await只暫停當(dāng)前的異步函數(shù)
console.log('World!');
}
delayedGreeting();
console.log('Goodbye!');
上面的代碼會(huì)依次打印出:
Hello
Goodbye!
World!
這樣,你可以根據(jù)需要靈活地使用不同的方法和技術(shù)來(lái)實(shí)現(xiàn)JavaScript中的延遲和異步操作。
創(chuàng)建 JS Sleep函數(shù)的最佳實(shí)踐
我們已經(jīng)探討了各種在JavaScript中引入延遲的方法。現(xiàn)在讓我們總結(jié)一下哪種方法最適合不同的場(chǎng)景,以及哪種方法通常應(yīng)該避免。
1.純粹的setTimeout
console.log('Hello');
setTimeout(() => { console.log('World!'); }, 2000);
?? 優(yōu)點(diǎn):容易理解,非阻塞。 ?? 缺點(diǎn):對(duì)異步操作的控制有限。 ?? 何時(shí)使用:適用于簡(jiǎn)單的、一次性的延遲,或基礎(chǔ)輪詢。
2.遞增的 setTimeout
setTimeout(() => { console.log('Hello'); }, 1000);
setTimeout(() => { console.log('World!'); }, 2000);
?? 優(yōu)點(diǎn):非阻塞性,易于實(shí)現(xiàn),不需要了解 promises 或 async/await。 ?? 缺點(diǎn):不適用于復(fù)雜的異步操作。沒(méi)有錯(cuò)誤處理。 ?? 何時(shí)使用:用于有時(shí)間間隔的簡(jiǎn)單序列。
3.通過(guò)循環(huán)阻塞事件循環(huán)
console.log('Hello');
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < 2000);
console.log('World!');
?? 優(yōu)點(diǎn):模仿傳統(tǒng)的sleep行為。 ?? 缺點(diǎn):阻塞整個(gè)線程,可能會(huì)凍結(jié)UI或?qū)е鲁绦虮罎ⅰ??? 強(qiáng)烈不推薦:只有在你絕對(duì)需要暫停執(zhí)行并且意識(shí)到其中的風(fēng)險(xiǎn)時(shí)才使用。
4.使用Promises與setTimeout
const sleep = function(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
console.log('Hello');
sleep(2000).then(() => { console.log('World!'); });
?? 優(yōu)點(diǎn):非阻塞性,對(duì)異步操作有更多的控制。 ?? 缺點(diǎn):需要理解promises。更長(zhǎng)的promise鏈可能會(huì)變得有點(diǎn)混亂。 ?? 何時(shí)使用:當(dāng)你需要更多對(duì)時(shí)間和異步操作的控制時(shí)。
5.使用async/await與Promises
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedGreeting() {
console.log('Hello');
await sleep(2000);
console.log('World!');
await sleep(2000);
console.log('Goodbye!');
}
delayedGreeting();
?? 優(yōu)點(diǎn):語(yǔ)法清晰,易于閱讀,非阻塞。 ?? 缺點(diǎn):需要理解async/await和promises。需要在模塊外部“包裝”函數(shù)。 ? 強(qiáng)烈推薦:這是最現(xiàn)代和干凈的方法,尤其是在處理多個(gè)異步操作時(shí)。
總結(jié)
JavaScript中的時(shí)序問(wèn)題是許多開(kāi)發(fā)人員頭疼的原因,你如何處理它們?nèi)Q于你想實(shí)現(xiàn)什么。
盡管在許多其他語(yǔ)言中都有 sleep 函數(shù),但我鼓勵(lì)你去接受JavaScript的異步特性,盡量不要與這門語(yǔ)言作對(duì)。當(dāng)你習(xí)慣了它,它實(shí)際上是相當(dāng)不錯(cuò)的。