JS 封装三种循环执行函数
文章目录
1. 问题引入
在开发场景中,我们经常遇到如下类似需求:
-
实现打字机效果:
每 50ms 在文本域中插入一个字符。
来源:《BRICS-FS-34_数字媒体交互设计》
-
实现间隔移动效果:
每 150ms 删除当前方格的 class,设置下一个方格的 class 为
active
,直到达到最后一个方格。来源:《第十四届蓝桥杯国赛(Web 应用开发) 大学组 - “恶龙与公主”》
2. 解决办法
一般遇到这种需求,我的第一反应是使用定时器来实现,如下代码:
// 定义 count 和 duration
let count = 10;
let duration = 150;
// 每次执行的方法
function execute() {
console.log(count);
}
// 立即执行
execute();
// 定时器
let timer = setInterval(() => {
count--;
if (count <= 0) {
clearInterval(timer);
return;
}
execute(count);
}, duration);
循环执行一个方法 count
次,同时每次执行的时间间隔为 duration
。代码中在 setInterval
之前执行 execute
目的是保证 立即执行
一次函数。
3. 提出需求
现在,我们想封装一个函数,要做到如下要求:
- 接收
count
、duration
和callback
;- count:循环次数
- duration:时间间隔
- callback:每次执行的回调
- 进阶:实现回调/调用点阻塞,即循环完成后再执行后续代码。
4. 循环执行函数
循环执行函数指的是在程序中反复执行特定操作的一种机制。这种函数可以在不需要手动重复调用的情况下,自动地多次执行相同的任务。
通常情况下,循环执行函数会包含一个循环结构,使得其中的某段代码会被重复执行多次,直到满足某个终止条件为止。这种函数通常用于需要反复执行相同操作的场景,比如定时任务、轮询、事件监听等。
循环执行函数的实现方式多种多样,可以利用 JavaScript 中的各种语法和异步机制来实现。本文章将介绍三种常见的方法:
- 使用
Promise
实现; - 使用
setTimeout
实现; - 使用
setInterval
实现。
5. 代码
5.1. 使用 Promise
/**
* 使用 Promise 实现循环执行函数
*
* @param {number} count - 循环次数
* @param {number} duration - 时间间隔
* @param {Function} callback - 每次执行的回调
*/
async function loopWithPromise(count, duration, callback) {
// 循环执行 count 次
for (; count > 0; count--) {
// 调用回调函数,并传入当前次数
callback(count);
// 等待 duration 后继续执行,使用 Promise 和 setTimeout 实现异步延迟
await new Promise(resolve => setTimeout(resolve, duration));
}
}
函数内部使用了 await
关键字,它会暂停函数的执行,直到 Promise
对象被解析为 resolved
状态,才继续下一次回调。
测试:
(async () => {
// 一定要加上 await
await loopWithPromise(5, 300, (count) => {
console.log("执行次数: " + count);
});
console.log("执行完成!");
})();
// 执行次数: 5
// 执行次数: 4
// 执行次数: 3
// 执行次数: 2
// 执行次数: 1
// 执行完成!
如果我们删除掉测试代码中的 async
和 await
,运行结果将变成这样:
// 执行次数: 5
// 执行完成!
// 执行次数: 4
// 执行次数: 3
// 执行次数: 2
// 执行次数: 1
可以看到,我们在立即执行函数上添加了 async
,在调用前添加了 await
,使得我们的循环函数可以阻塞调用点,循环完成之后,才执行后续代码。
在线运行:
5.2. 使用 setTimeout
/**
* 使用 setTimeout 实现循环执行函数
*
* @param {number} count - 循环次数
* @param {number} duration - 时间间隔
* @param {Function} callback - 每次执行的回调
* @param {Function} onComplete - 循环完成后的回调
*/
function loopWithTimeout(count, duration, callback, onComplete) {
// 定义内部执行函数
function execute() {
// 调用回调函数,并传入当前次数
callback(count);
// 减少次数
count--;
// 如果次数大于 0,则延迟一定时间后继续执行
if (count > 0) setTimeout(execute, duration);
// 调用循环完成后的回调函数
else if (onComplete) onComplete();
}
// 如果次数大于 0,则开始执行循环
if (count > 0) execute();
}
测试:
(() => {
loopWithTimeout(5, 300, (count) => {
console.log("执行次数: " + count);
}, () => {
console.log("执行完成!");
});
console.log("后续代码!");
})();
// 执行次数: 5
// 后续代码!
// 执行次数: 4
// 执行次数: 3
// 执行次数: 2
// 执行次数: 1
// 执行完成!
可以看到,在执行一次循环后,就输出了 “后续代码!”,这是因为 setTimeout
是异步执行的,它会在指定的时间间隔后把任务添加到事件队列中,而不是立即执行。
所以,我们需要传入 onComplete
回调函数,在里面执行后续代码。
在线运行:
5.3. 使用 setInterval
/**
* 使用 setInterval 实现循环执行函数
*
* @param {number} count - 循环次数
* @param {number} duration - 时间间隔
* @param {Function} callback - 每次执行的回调
* @param {Function} onComplete - 循环完成后的回调
*/
function loopWithInterval(count, duration, callback, onComplete) {
// 设置定时器,每隔 duration 执行一次
let timer = setInterval(execute, duration);
// 定义内部执行函数
function execute() {
// 如果次数小于等于 0,则清除定时器并调用循环完成后的回调函数
if (count <= 0) {
clearInterval(timer);
onComplete && onComplete();
return;
}
// 调用回调函数,并传入当前次数
callback(count);
// 减少次数
count--;
}
// 立即执行一次函数
execute();
}
测试:
(() => {
loopWithInterval(5, 300, (count) => {
console.log("执行次数: " + count);
}, () => {
console.log("执行完成!");
});
console.log("后续代码!");
})();
// 执行次数: 5
// 后续代码!
// 执行次数: 4
// 执行次数: 3
// 执行次数: 2
// 执行次数: 1
// 执行完成!
解释同方法二。
在线运行:
6. 总结
方法一通过 async
和 await
避免回调地狱,使代码更加清晰和易于理解。通过 await
等待异步操作的结果,而不是通过传递回调函数,使得代码逻辑更加线性化。
方法二方法三需要在 onComplete
回调中实现后续代码,可能会不利于阅读/导致代码臃肿。
可以根据实际需求场景,选择合适的方法来实现循环执行功能。
7. 拓展:实现 sleep 功能
async function sleep(duration) {
return new Promise(resolve => setTimeout(resolve, duration));
}
测试:
(async () => {
console.log("第一次执行:", getTime());
await sleep(500);
console.log("第二次执行:", getTime());
await sleep(1000);
console.log("第三次执行:", getTime());
await sleep(1500);
console.log("执行完成!", getTime());
function getTime() {
const date = new Date();
return `${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}`;
}
})();
// 第一次执行: 35:56.894
// 第二次执行: 35:57.395
// 第三次执行: 35:58.399
// 执行完成! 35:59.906
需要在异步函数中使用,同时 sleep
方法之前需要添加 await
语法糖。
在线运行: