for循环中的setTimeout

1. 错误示例

1
2
3
4
5
6
7
function print() {
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
}

上面的代码,我们期望每隔一秒,输出一个数,而且这个数是每次循环i的值。但是期望也只是期望,实际情况是一秒钟之后直接输出了10个10。那么问题就来了,我们怎么实现我们的期望值呢?

由此我们先来尝试几种解决方案,并作一一对比:

  • 每隔一秒输出一个10
    1
    2
    3
    4
    5
    6
    7
    8
    // 每隔一秒输出一个10
    function print () {
    for (var i = 0; i < 10; i++) {
    setTimeout(() => {
    console.log(i);
    }, i * 1000);
    }
    }
  • 等待一秒之后接连输出0-9
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 等待一秒之后接连输出0-9
    function print () {
    for (var i = 0; i < 10; i++) {
    (function (i) {
    setTimeout(() => {
    console.log(i);
    }, 1000);
    })(i);
    }
    }

综上所述,第一种方式实现了每隔一秒输出一个数值的功能,第二种方案实现了每次输出不同的i值的功能,如果将这两种方案综合起来,那就是我们想要的结果,即:

1
2
3
4
5
6
7
8
9
10
// 每隔一秒输出一个自增的i值
function print () {
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, i * 1000);
})(i);
}
}

2. 问题及解决办法

之所以会出现这种情况差异,是因为 js 是单线程的,有一个事件队列机制,setTimeout 和 setInterval 的回调会到了延迟时间塞入事件队列中,排队执行。而在此同时,for循环并不会一直等待,而会直接循环完毕,所以等到 setTimeout 开始执行的时候,原先我们预想的i早就变成了10,所以就会出现之前我们遇到的问题。

为了实现我们想要的功能,就要解决两个问题:

    1. 怎么保存每次循环的临时变量 i
    1. 怎么让定时器产生时间间隔

弄懂了我们要解决的问题,那么接下来敲代码就行云流水了,关于第一个问题,我们需要保存每次循环 i 的值,关于第二个问题,我们只需要控制定时器的时间就好,以下即是几种解决方案:

3. 正确示例

1. 添加一个函数调用

1
2
3
4
5
6
7
8
9
10
11
12
function print () {
for (var i = 0; i < 10; i++) {
timePrint(i); // 这里的 i 值被复制了
}
}

// 每次传进来的 i 值都不同,故可得以保存
function timePrint (i) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}

2. 立即执行函数(闭包)

1
2
3
4
5
6
7
8
9
 function print () {
for (var i = 0; i < 10; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, i * 1000);
})(i);
}
}

3. setTimeout另一种用法

1
2
3
4
5
6
7
function print() {
for (var i = 0; i < 10; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000 * i, i); // 从第三个参数开始的附加参数都可以传给setTimeout的匿名函数
}
}

setTimeout()的返回值是一个正整数,表示定时器的编号。这个值可以传递给clearTimeout()来取消该定时器。需要注意的是setTimeout()和setInterval()共用一个编号池,技术上,clearTimeout()和 clearInterval() 可以互换。但是,为了避免混淆,不要混用取消定时函数。

4. let方法保存不同阶段的值

1
2
3
4
5
6
7
function print() {
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
}

5. Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const tasks = [];

for (var i = 0; i < 10; i++) {
(function(j) {
tasks.push(new Promise(resolve => {
setTimeout(() => {
console.log(j);
resolve();
}, 1000 * j);
}))
})(i);
}

Promise.all(tasks);

6. 简洁版的Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
const tasks = [];
const output = (i) => new Promise(resolve => {
setTimeout(() => {
console.log(i);
resolve();
}, 1000 * i);
})

for (var i = 0; i < 10; i++) {
tasks.push(output(i));
}

Promise.all(tasks);

7. sleep方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 模拟其他语言的sleep方法
const sleep = (time) => new Promise(resolve => {
setTimeout(resolve, time);
})

async function print () {
for (var i = 0; i < 10; i++) {
await sleep(1000);
console.log(i);
}
};

print();

8. bind方法

1
2
3
4
5
6
7
function print() {
for (var i = 0; i < 10; i++) {
setTimeout(function(i) {
console.log(new Date, i);
}.bind(null,i), 1000 * i);
}
}