『JavaScript』封装三种循环执行函数

本文介绍了如何使用JavaScript的Promise、setTimeout和setInterval实现循环执行函数,包括满足回调/调用点阻塞的需求,并提供了示例代码和拓展的sleep功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JS 封装三种循环执行函数

1. 问题引入

在开发场景中,我们经常遇到如下类似需求:

  1. 实现打字机效果:

    每 50ms 在文本域中插入一个字符。

    打字机效果

    来源:《BRICS-FS-34_数字媒体交互设计》

  2. 实现间隔移动效果:

    每 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. 提出需求

现在,我们想封装一个函数,要做到如下要求:

  1. 接收 countdurationcallback
    1. count:循环次数
    2. duration:时间间隔
    3. callback:每次执行的回调
  2. 进阶:实现回调/调用点阻塞,即循环完成后再执行后续代码

4. 循环执行函数

循环执行函数指的是在程序中反复执行特定操作的一种机制。这种函数可以在不需要手动重复调用的情况下,自动地多次执行相同的任务。

通常情况下,循环执行函数会包含一个循环结构,使得其中的某段代码会被重复执行多次,直到满足某个终止条件为止。这种函数通常用于需要反复执行相同操作的场景,比如定时任务、轮询、事件监听等。

循环执行函数的实现方式多种多样,可以利用 JavaScript 中的各种语法和异步机制来实现。本文章将介绍三种常见的方法:

  1. 使用 Promise 实现;
  2. 使用 setTimeout 实现;
  3. 使用 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
// 执行完成!

如果我们删除掉测试代码中的 asyncawait,运行结果将变成这样:

// 执行次数: 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. 总结

方法一通过 asyncawait 避免回调地狱,使代码更加清晰和易于理解。通过 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 语法糖。

在线运行:

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值