3

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

 2 years ago
source link: https://www.xiabingbao.com/post/fe/loop-settimeout-rg18mv.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

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

一个很经典的错误案例:

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

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

在指定 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()

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);
  }
};

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK