异步编程


定时器

在 JS 中,我们可能会需要延迟执行某些逻辑,或者定时执行某些逻辑。JS 中内置了两个定时器函数,用来处理这种情况:

function foo() {
  console.log("延迟执行");
}

setTimeout(foo, 1000);

定时器函数的第一个参数是一个回调函数,上面代码中,我们使用 setTimeout 定时器在 1000 毫秒后执行 foo 函数。

定时器有两个 setTimeoutsetIntervalsetTimeout 只会执行一次,setInterval 会重复执行,直到你主动关闭定时器。

演示 setTimeoutsetInterval

function foo() {
  console.log("我只执行了一次");
}

function bar() {
  console.log("我执行了多次");
}

setTimeout(foo, 1000);
setInterval(bar, 1000);

使用函数表达式简化上述代码:

setTimeout(function () {
  console.log("我只执行了一次");
}, 1000);

setInterval(function () {
  console.log("我执行了多次");
}, 1000);

关闭定时器

关闭定时器使用 clearTimeoutclearInterval。定时器函数会返回一个数字,用来关闭定时器:

var t1 = setTimeout(function () {
  console.log("我只执行了一次");
}, 1000);

clearTimeout(t1);

上述代码并不会打印任何数据,因为定时器被关闭了。

你还可以在定时器内部关闭定时器:

var i = 0;
var t1 = setInterval(function () {
  if (i++ > 10) {
    clearInterval(t1);
  } else {
    console.log(i);
  }
}, 500);

事件队列

查看以下代码:

setTimeout(function () {
  console.log(1);
}, 0);
console.log(2);

上述代码中,2 会在 1 之前打印。即使如果给定时器的时间定义为 0,它也不会立即执行,它会等到当前代码都处理结束之后才去执行。

你可以尝试编写一个非常长的循环占用资源,来判断这个情况:

setTimeout(function () {
  console.log(1);
}, 0);
console.log(2);
for (var i = 0; i < 1000000000; i++) {}

发送这种情况的原因是因为定时器回调函数的代码是异步的,异步代码不在当前代码的执行栈中。

JS 中的代码的执行单位是栈,代码是一栈一栈运行的,当前栈全部运行结束,才会运行下一栈。 这种执行方式就好像银行办理手续的队伍,只有最前面的人办理结束后,后面的人才能接着办理。其中每一个人就代表一栈代码,办理手续的人代表代码正在执行,正在执行的栈被称作 当前执行栈

定时器中的代码就会被分到队伍的末尾,如:

// 程序一开始首栈

// 定时器回调函数在 0 秒后,放到队伍末尾
setTimeout(function () {
  console.log(1);
}, 0);

// 定时器回调函数在 0 秒后,放到队伍末尾
setTimeout(function () {
  console.log(2);
}, 0);

console.log(3);

// 首栈运行结束,按顺序开始运行下一栈

栈与栈之间是按顺序排列的,依次形成一个队伍,最前面的被执行,后来的只能在末尾等候。这个队伍在 JS 中叫做 异步队列 或者 事件队列

需要注意的是,定时器延时执行的代码,是延时后把回调函数放到事件队列中。设置一个 10 分钟的定时器,代码会在 10 分钟后加入到事件队列的末尾。就类似一个人到了银行,它没有立即取号,而是在 10 分钟后才取号,此时他才开始排队。

还有一点就是,不管把代码分开写在多少个 script 元素上,全局上的代码都是在首栈中。

JS 中针对事件队列,有一个并发模型,如果你很有兴趣,可以点击 这里