我们在上一篇文章中聊了多种 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()。
从远吗中也能看到,这是对数组的每一项都调用了 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); } };
顺带地,我们也就知道了为什么break
,return
等终止循环的语句在 forEach()中没有效果了。因为这些操作语句的作用范围仅是限制在回调函数 callback 的内部,并不会影响到外层的循环。
既然 Promise 和循环可以结合,那么 Promise 和递归也可以结合。这里我们就不拆解了。
6. 总结
我们用了多种方法来实现这样的功能,不同的方法接触到的知识点也不一样。