我们在上一篇文章中聊了多种 Promise 的并发控制,于是就衍生出了另一个问题:定时输出一系列的内容,这里可能是数组中的数据,也可能是其他的。我们来看看有哪些实现方法。
既然是定时输出,必然涉及到 setTimeout 或者 setInterval 的运用。
一个很经典的错误案例:
// 使用var声明变量
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 500 * i);
}
var
声明的变量存在变量提升的问题,而且 for 循环是同步任务,当执行 setTimeout 时,for 循环已执行完毕,因此输出的全是 10。那有什么解决方案吗?
1. 闭包 #
在指定 setTimeout 时,外层包一个闭包,当 setTimeout 向外层寻找时,找到该闭包就停止了,而每个闭包中的环境是独立的。
for (var i = 0; i < 10; i++) {
((j) => {
setTimeout(() => {
console.log(j);
}, 500 * j);
})(i);
}
2. 使用 let 来声明变量 #
主要考虑 let 的块级作用域和 eventloop 事件循环机制。如:
// 使用let声明变量
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 500 * i);
}
let
声明的变量是块级作用域,setTimeout 向外层寻找到的变量 i 就是当时循环时的那个 i 的变量。
3. setTimeout 本身就可以传参 #
可能我们用 setTimeout 后续的参数比较少,其实 setTimeout() 函数,从第 3 个参数开始,都是传入到回调里的数据。如:
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 循环改成:
for (var i = 0; i < 10; i++) {
setTimeout(
(j) => {
console.log(j);
},
500 * i,
i,
);
}
4. bind #
我们可以 setTimeout 中的回调函数,通过 bind()方法再生成一个:
for (var i = 0; i < 10; i++) {
setTimeout(
((j) => {
console.log(j);
}).bind(null, i),
500 * i,
);
}
里面拆开一下:
const fn = (j) => {
console.log(j);
};
const callback = fn.bind(null, i);
setTimeout(callback, 500);
bind()本身就是用闭包来实现的。
5. Promise #
我们可以把 setTimeout 封装成一个 Promise,然后再在 for 循环里使用。
const sleep = (delay) => {
return new Promise((resolve) => setTimeout(resolve, delay));
};
循环所在的函数改为async-await
的结构:
const start = async () => {
for (var i = 0; i < 10; i++) {
// for-of也可以
await sleep(500);
console.log(i);
}
};
start();
我们用了普通的 for 循环,其实for-of
也是可以的。但forEach()
, map()
等方法就不可以了,如:
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()。
从远吗中也能看到,这是对数组的每一项都调用了 callback。
上面的写法,我们拆分一下就好理解了:
const callback = async (item) => {
await sleep(500);
console.log(item);
};
arr.forEach(callback);
若我们自己来实现 forEach() 方法时:
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);
}
};
顺带地,我们也就知道了为什么break
,return
等终止循环的语句在 forEach()中没有效果了。因为这些操作语句的作用范围仅是限制在回调函数 callback 的内部,并不会影响到外层的循环。
既然 Promise 和循环可以结合,那么 Promise 和递归也可以结合。这里我们就不拆解了。
6. 总结 #
我们用了多种方法来实现这样的功能,不同的方法接触到的知识点也不一样。