在 JavaScript 中循环和定时输出一系列的内容

蚊子前端博客
发布于 2022-08-03 17:20
基于js的机制,如何定时循环输出一系列的内容?

我们在上一篇文章中聊了多种 Promise 的并发控制,于是就衍生出了另一个问题:定时输出一系列的内容,这里可能是数组中的数据,也可能是其他的。我们来看看有哪些实现方法。

既然是定时输出,必然涉及到 setTimeout 或者 setInterval 的运用。

一个很经典的错误案例:

COPYJAVASCRIPT

// 使用var声明变量 for (var i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 500 * i); }

var声明的变量存在变量提升的问题,而且 for 循环是同步任务,当执行 setTimeout 时,for 循环已执行完毕,因此输出的全是 10。那有什么解决方案吗?

1. 闭包

在指定 setTimeout 时,外层包一个闭包,当 setTimeout 向外层寻找时,找到该闭包就停止了,而每个闭包中的环境是独立的。

COPYJAVASCRIPT

for (var i = 0; i < 10; i++) { ((j) => { setTimeout(() => { console.log(j); }, 500 * j); })(i); }

2. 使用 let 来声明变量

主要考虑 let 的块级作用域和 eventloop 事件循环机制。如:

COPYJAVASCRIPT

// 使用let声明变量 for (let i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 500 * i); }

let声明的变量是块级作用域,setTimeout 向外层寻找到的变量 i 就是当时循环时的那个 i 的变量。

3. setTimeout 本身就可以传参

可能我们用 setTimeout 后续的参数比较少,其实 setTimeout() 函数,从第 3 个参数开始,都是传入到回调里的数据。如:

COPYJAVASCRIPT

setTimeout( (username, age) => { console.log(`my name is ${username}, my age is ${age}`); // my name is jack, my age is 28 }, 500, // 延迟时间 'jack', // 从这里开始,都是参数,并且可以无限个 28, );

因此,我们可以把 for 循环改成:

COPYJAVASCRIPT

for (var i = 0; i < 10; i++) { setTimeout( (j) => { console.log(j); }, 500 * i, i, ); }

4. bind

我们可以 setTimeout 中的回调函数,通过 bind()方法再生成一个:

COPYJAVASCRIPT

for (var i = 0; i < 10; i++) { setTimeout( ((j) => { console.log(j); }).bind(null, i), 500 * i, ); }

里面拆开一下:

COPYJAVASCRIPT

const fn = (j) => { console.log(j); }; const callback = fn.bind(null, i); setTimeout(callback, 500);

bind()本身就是用闭包来实现的。

5. Promise

我们可以把 setTimeout 封装成一个 Promise,然后再在 for 循环里使用。

COPYJAVASCRIPT

const sleep = (delay) => { return new Promise((resolve) => setTimeout(resolve, delay)); };

循环所在的函数改为async-await的结构:

COPYJAVASCRIPT

const start = async () => { for (var i = 0; i < 10; i++) { // for-of也可以 await sleep(500); console.log(i); } }; start();

我们用了普通的 for 循环,其实for-of也是可以的。但forEach(), map()等方法就不可以了,如:

COPYJAVASCRIPT

const start = async () => { // for (var i = 0; i < 10; i++) { // // for-of也可以 // await sleep(500); // console.log(i); // } const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; // 错误的方式,请不要使用 arr.forEach(async (item) => { await sleep(500); console.log(item); }); }; start();

虽然输出了 0-9,但是是同时输出的,两个输出之间没有间隔。这是因为 forEach()中,每次循环的 callback 都是独立执行的,async 只控制其自己内部的 await,并不能控制其他的循环。

可以看下 V8 源码中的实现,Array.prototype.forEach()实际上调用的 V8 中的ArrayForEach()

ArrayForEach()的源码-蚊子的前端博客

从远吗中也能看到,这是对数组的每一项都调用了 callback。

上面的写法,我们拆分一下就好理解了:

COPYJAVASCRIPT

const callback = async (item) => { await sleep(500); console.log(item); }; arr.forEach(callback);

若我们自己来实现 forEach() 方法时:

COPYJAVASCRIPT

Array.prototype.forEach = function (callback, thisArg) { const context = thisArg ?? null; const arr = this; for (let i = 0; i < arr.length; i++) { callback.call(context, arr[i], i, arr); } };

顺带地,我们也就知道了为什么breakreturn等终止循环的语句在 forEach()中没有效果了。因为这些操作语句的作用范围仅是限制在回调函数 callback 的内部,并不会影响到外层的循环。

既然 Promise 和循环可以结合,那么 Promise 和递归也可以结合。这里我们就不拆解了。

6. 总结

我们用了多种方法来实现这样的功能,不同的方法接触到的知识点也不一样。

标签:
阅读(854)

公众号:

qrcode

微信公众号:前端小茶馆

公众号:

qrcode

微信公众号:前端小茶馆