首发于公众号 大迁世界,欢送关注。📝 每周 7 篇实用的前端文章 🛠️ 分享值得关注的开发工具 😜分享集体守业过程中的趣事
许多编程语言都有一个 sleep
函数,能够提早程序的执行若干秒。JavaScript 短少这个内置性能,但不必放心。在这篇文章中,咱们将探讨在 JavaScript 代码中实现提早的各种技巧,同时思考到该语言的异步性质。
如何在 JS 中创立 sleep 函数
对于那些只想疾速解决问题而不想深刻理解技术细节的人,咱们也有简单明了的解决方案。上面是如何在你的 JavaScript 工具箱中增加一个 sleep
函数的最间接形式:
function sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}
console.log('Hello');
sleep(2000).then(() => { console.log('World!'); });
运行这段代码,你会在控制台看到“Hello”。而后,在短暂的两秒钟后,“World!”v 会接着呈现。这是一种既简洁又无效的引入提早的办法。
如果你只是为了这个来的,那太好了!但如果你对“为什么”和“怎么做”的起因感到好奇,还有更多能够学习的内容。JavaScript 中解决工夫有其轻微之处,理解这些可能会对你有所帮忙。
了解 JavaScript 的执行模型
当初咱们曾经有了一个疾速的解决方案,让咱们深刻理解 JavaScript 的执行模型的机制。了解这一点对于无效地治理代码中的工夫和异步操作至关重要。
思考以下 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 发送一个申请以获取我的用户数据。而后解析响应,输入与我的 GitHub 帐户关联的公共仓库的数量,最初在屏幕上打印“Hello!”。执行是从上到下进行的。
相比之下,这是雷同性能的 JavaScript 版本:
fetch('https://api.github.com/users/jameshibbard')
.then(res => res.json())
.then(json => console.log(json.public_repos));
console.log('Hello!');
如果你运行这段代码,它会先在屏幕上输入“Hello!”,而后输入与我的 GitHub 帐户关联的公共仓库的数量。
这是因为在 JavaScript 中,从 API 获取数据是一个异步操作。JavaScript 解释器会遇到 fetch 命令并发送申请。然而,它不会期待申请实现。相同,它会继续执行,将“Hello!”输入到控制台,而后当申请在几百毫秒后返回时,它会输入仓库的数量。
如何在 JavaScript 中正确应用 SetTimeout
既然咱们曾经更好地了解了 JavaScript 的执行模型,让咱们看看 JavaScript 是如何解决提早和异步代码的。
在 JavaScript 中创立提早的规范办法是应用其 setTimeout
办法。例如:
console.log('Hello');
setTimeout(() => { console.log('World!'); }, 2000);
这将在管制台上输入 “Hello”,而后两秒后输入 “World!”。在很多状况下,这曾经足够了:做某事,而后在短暂的提早后,做其余事件。问题解决!
但可怜的是,事件并不总是那么简略。
你可能会认为 setTimeout
会暂停整个程序,但事实并非如此。它是一个异步函数,这意味着其余的代码不会期待它实现。
例如,假如你运行了以下代码:
console.log('Hello');
setTimeout(() => { console.log('World!'); }, 2000);
console.log('Goodbye!');
你会看到以下输入:
Hello
Goodbye!
World!
留神“Goodbye!”是如何呈现在“World!”之前的?这是因为 setTimeout
不会阻塞其余代码的执行。
这意味着你不能这样做:
console.log('Hello');
setTimeout(1000);
console.log('World');
“Hello” 和 “World” 会立刻被记录在管制台上,之间没有显著的提早。
你也不能这样做:
for (let i = 0; i < 5; i++) {setTimeout(() => {console.log(i); }, i * 1000);
}
花一秒钟考虑一下下面的代码片段可能会产生什么。
它不会在每个数字之间提早一秒钟打印数字 0
到 4。相同,你实际上会失去五个 4,它们在四秒后一次性全副打印进去。为什么呢?因为循环不会暂停执行。它不会期待 setTimeout
实现才进入下一次迭代。
那么 setTimeout 实际上有什么用呢?当初让咱们来看看。
setTimeout() 函数的检查和最佳实际
正如你能够在咱们的 setTimeout
教程中浏览到的,原生 JavaScript setTimeout
函数在指定的提早(以毫秒为单位)后调用一个函数或执行一个代码片段。
这可能在某些状况下是有用的,例如,如果你心愿在访问者浏览你的页面一段时间后显示一个弹出窗口,或者你心愿在从元素上移除悬停成果之前有短暂的提早(以防用户意外地鼠标移出)。
setTimeout
办法承受一个函数的援用作为第一个参数。
这能够是函数的名称:
function greet(){alert('Howdy!');
}
setTimeout(greet, 2000);
它能够是一个指向函数的变量(函数表达式):
const greet = function(){alert('Howdy!');
};
setTimeout(greet, 2000);
或者它能够是一个匿名函数(在这种状况下是箭头函数):
setTimeout(() => { alert('Howdy!'); }, 2000);
也能够将一段代码字符串传递给 setTimeout
以供其执行:
然而,这种办法是不可取的,因为:
- 它很难浏览(因而很难保护和 / 或调试)
- 它应用了一个隐含的
eval
,这是一个潜在的平安危险 - 它比代替计划慢,因为它必须调用 JS 解释器
如前所述,setTimeout
非常适合在提早后触发一次性操作,但也能够应用 setTimeout
(或其表亲 setInterval
)来让 JavaScript 期待直到满足某个条件。例如,上面是如何应用 setTimeout
期待某个元素呈现在网页上的形式:
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();
这假如该元素最终会呈现。如果你不确定这是否会产生,你须要思考勾销计时器(应用 clearTimeout
或 clearInterval
)。
在 JS 中应用递增超时作为 Sleep 函数的代替计划
有时,你可能会发现自己想要在一系列操作中引入提早。尽管你能够应用各种办法来模仿一个 Sleep 函数,但还有另一种常常被忽视的办法:递增超时。
这个思路很简略:你不是暂停整个执行线程,而是应用 setTimeout
为每个后续操作减少提早。这样,你能够创立一个提早操作的序列,而不会阻塞浏览器或侵害用户体验。
上面是一个疾速示例:
let delay = 1000; // 从 1 秒的提早开始
for (let i = 0; i < 5; i++) {setTimeout(() => {console.log(` 这是音讯 ${i + 1}`);
}, delay);
delay += 1000; // 每次迭代提早减少 1 秒
}
在这个示例中,第一条音讯将在 1 秒后呈现,第二条音讯在 2 秒后,依此类推,直到第五条音讯在 5 秒后。
这种办法的长处是它不阻塞,易于实现,并且不须要理解 promises
或 async/await
。然而,它不适用于须要准确计时或错误处理的简单异步操作
古代 JavaScript 中的流控制
编写 JavaScript 时,咱们常常须要期待某件事情产生(例如,从 API 获取数据),而后做出响应(例如,更新 UI 以显示数据)。
下面的示例应用了一个匿名回调函数来实现这一目标,但如果你须要期待多个事件产生,语法很快就会变得相当简单,你最终会陷入回调天堂。
侥幸的是,这门语言在过来几年里有了很大的倒退,当初为咱们提供了新的结构来防止这一点。
例如,应用 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!');
})();
当初,代码从上到下执行。JavaScript 解释器期待网络申请实现,首先记录公共仓库的数量,而后记录“Hello!”音讯。
将 Sleep 函数引入原生 JavaScript
如果你还在看这篇文章,那么我猜你肯定是想阻塞那个执行线程,并让 JavaScript 期待一下。
上面是你可能会这样做的形式:
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!');
正如预期的那样,这将在管制台上打印“Hello”,暂停两秒,而后打印“World!”
它通过应用 Date.now
办法获取自 1970 年 1 月 1 日以来通过的毫秒数,并将该值调配给一个 date
变量。而后它创立一个空的 currentDate
变量,而后进入一个 do ... while
循环。在循环中,它会反复获取自 1970 年 1 月 1 日以来通过的毫秒数,并将该值调配给之前申明的 currentDate
变量。只有 date
和 currentDate
之间的差别小于所需的毫秒数的提早,循环就会持续进行。
工作实现了,对吗?好吧,也不齐全是……
如何在 JavaScript 中编写更好的 Sleep 函数
兴许这段代码正是你所冀望的,但请留神,它有一个很大的毛病:循环会阻塞 JavaScript 的执行线程,并确保在它实现之前没有人能与你的程序进行交互。如果你须要很大的提早,甚至有可能会让整个程序解体。
那么应该怎么做呢?
事实上,也能够联合本文后面学到的技巧来制作一个不太侵入性的 sleep
办法:
function sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}
console.log('Hello');
sleep(2000).then(() => { console.log('World!'); });
这段代码将在管制台上打印“Hello”,期待两秒,而后打印“World!”在底层,咱们应用 setTimeout
办法在给定的毫秒数后解析一个 promise
。
留神,咱们须要应用一个 then
回调来确保第二条音讯是带有提早的。咱们还能够在第一个回调函数前面链式地增加更多回调函数。
这样做是可行的,但看起来不太好看。咱们能够应用 async ... await
来丑化它:
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();
这看起来更好看,但这意味着应用 sleep
函数的任何代码都须要被标记为 async
。
当然,这两种办法依然有一个毛病(或特点),那就是它们不会暂停整个程序的执行。只有你的函数会睡眠:
function sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}
async function delayedGreeting() {console.log('Hello');
await sleep(2000); // await 只暂停以后的异步函数
console.log('World!');
}
delayedGreeting();
console.log('Goodbye!');
下面的代码会顺次打印出:
Hello
Goodbye!
World!
这样,你能够依据须要灵便地应用不同的办法和技术来实现 JavaScript 中的提早和异步操作。
创立 JS Sleep 函数的最佳实际
咱们曾经探讨了各种在 JavaScript 中引入提早的办法。当初让咱们总结一下哪种办法最适宜不同的场景,以及哪种办法通常应该防止。
1. 纯正的 setTimeout
console.log('Hello');
setTimeout(() => { console.log('World!'); }, 2000);
👍 长处:容易了解,非阻塞。
👎 毛病:对异步操作的管制无限。
📝 何时应用:实用于简略的、一次性的提早,或根底轮询。
2. 递增的 setTimeout
setTimeout(() => { console.log('Hello'); }, 1000);
setTimeout(() => { console.log('World!'); }, 2000);
👍 长处:非阻塞性,易于实现,不须要理解 promises
或 async/await
。
👎 毛病:不适用于简单的异步操作。没有错误处理。
📝 何时应用:用于有工夫距离的简略序列。
3. 通过循环阻塞事件循环
console.log('Hello');
const date = Date.now();
let currentDate = null;
do {currentDate = Date.now();
} while (currentDate - date < 2000);
console.log('World!');
👍 长处:模拟传统的 sleep 行为。
👎 毛病:阻塞整个线程,可能会解冻 UI 或导致程序解体。
⚠️ 强烈不举荐:只有在你相对须要暂停执行并且意识到其中的危险时才应用。
4. 应用 Promises 与 setTimeout
const sleep = function(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}
console.log('Hello');
sleep(2000).then(() => { console.log('World!'); });
👍 长处:非阻塞性,对异步操作有更多的管制。
👎 毛病:须要了解 promises。更长的 promise 链可能会变得有点凌乱。
📝 何时应用:当你须要更多对工夫和异步操作的管制时。
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();
👍 长处:语法清晰,易于浏览,非阻塞。
👎 毛病:须要了解 async/await 和 promises。须要在模块内部“包装”函数。
✅ 强烈推荐:这是最古代和洁净的办法,尤其是在解决多个异步操作时。
总结
JavaScript 中的时序问题是许多开发人员头疼的起因,你如何解决它们取决于你想实现什么。
只管在许多其余语言中都有 sleep
函数,但我激励你去承受 JavaScript 的异步个性,尽量不要与这门语言作对。当你习惯了它,它实际上是相当不错的。
交换
首发于公众号 大迁世界,欢送关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑难?我来答复
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。