JavaScript 是一个单线程的语言,也就是说它同一时间只能执行一段代码,接下来我们通过两个例子说明一下单线程语言和多线程语言的区别。
setTimeout()
setTimeout 代码单线程运行机制:
/**
* setTimeout 执行是要等主线线程的流程执行完毕之后才会进行,并且按照 setTimeout 设置的顺序进行排队执行。
* 如果某一个 setTimeout 进行大量的计算,那么它就会阻塞在当前的 setTimeout 回调函数中,等待该计算完成后,再执行下一个 setTimeout 的回调函数。
*/
setTimeout(() => {
console.log(‘setTimeout – a’);
},0);
console.log(1);
console.log(2);
setTimeout(() => {
for (let i = 0; i < 10000022200; i++){}
console.log(‘setTimeout – b’);
},0);
console.log(3);
setTimeout(() => {
console.log(‘setTimeout – c’);
},0);
console.log(4);
setTimeout(() => {
console.log(‘setTimeout – d’);
},0);
console.log(5);
for (let i = 0; i < 10000222200; i++) {} // 一直等待它执行完毕后,才会执行 setTimeout 的回调。
%20%E3%80%81setTimeout()%E3%80%81setInterval()%20%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/imgs/762547.png)
从运行结果上可以看出,虽然 setTimeout – a 是写在代码当最开头,延时时间也为 0,但是,它并没有立即执行;而是等主逻辑的代码执行完毕后才进行调用的,当代码运行到 25 行的时候,由于这里有一个长长的循环,所以这里会阻塞等待一段时间,才会运行到第一个 setTimeout。setTimeout 的运行顺序是根据你代码中编写的顺序和延时时间决定的,下面通过一张图来说明上述代码的运行机制:
%20%E3%80%81setTimeout()%E3%80%81setInterval()%20%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/imgs/452d9c2b-0edf-48b4-b51d-71ccabd4a60e.png)
从运行结果中,可以看出如果主逻辑代码没有执行完毕,setTimeout 的回调函数是永远不会触发的,这就是单线程,它同一时间只能做一件事。
** 注意:如果主逻辑的代码执行时间已经超过了 setTimeout 第二个参数设置的 timeout 时间,那么等运行到该回调函数时,它会忽略掉这个时间,并立即执行。下面通过一段代码进行验证:
/**
* setTimeout 执行是要等主线线程的流程执行完毕之后才会进行,并且按照 setTimeout 设置的顺序进行排队执行。
* 如果某一个 setTimeout 进行大量的计算,那么它就会阻塞在当前的 setTimeout 回调函数中,等待该计算完成后,再执行下一个 setTimeout 的回调函数。
*
* 执行顺序:即使 setTimeout 放在最前面执行,它也是等到主线程执行完毕后,才运行,这就是单线程运行机制。
* setTimeout 中的第二个参数 timeout 这个延时时间,是一个相对时间,如果主线程运行的时间,已经超过了这个时间,那么执行到这个 setTimeout 的时候,会忽略这个时间,直接调用函数。
*/
setTimeout(() => {
console.log(‘setTimeout – a’);
},0);
setTimeout(() => {
for (let i = 0; i < 10000022200; i++){}
console.log(‘setTimeout – b’);
},0);
setTimeout(() => {
console.log(‘setTimeout – c’);
},0);
setTimeout(() => {
console.log(‘setTimeout – d’);
},10000);
console.log(1);
console.log(2);
console.log(3);
console.log(4);
console.log(5);
for (let i = 0; i < 10000222200; i++) {} // 一直等待它执行完毕后,才会执行 setTimeout 的回调。
%20%E3%80%81setTimeout()%E3%80%81setInterval()%20%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/imgs/46885403.png)
从上述运行结果可以看出,即使 setTimeout 放在主逻辑到最前边,但是它依然是要等到主逻辑到代码完全执行完毕后才执行。
由于 29 行有大量的循环逻辑,主逻辑大概会阻塞 20 秒钟左右,所以当调用到 19 行的 setTimeout 的回调函数时,会忽略掉它设置 timeout 参数,并立即执行该回调函数。
下面我们通过一段 Java 的代码演示一下多线程,以此说明一下单线程与多线程的区别:
Java 多线程代码运行机制:
public class Main {
public static void main(String[] args) {
// 控制台打印输出
System.out.println(“1”);
// 启动一个线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(“ 线程 1 before”);
for(int i = 0; i < 2099222220L; i++) {}
System.out.println(“ 线程 1 after”);
}
}).start();
// 控制台打印输出
System.out.println(“2”);
// 启动一个线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(“ 线程 2 ”);
}
}).start();
// 控制台打印输出
System.out.println(“3”);
// 通过一个大的循环阻塞主线程一段时间,看看会不会影响线程的运行
for(int i = 0; i < 2099222220L; i++) {}
// 控制台打印输出
System.out.println(“4”);
}
}
%20%E3%80%81setTimeout()%E3%80%81setInterval()%20%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/imgs/3722023.png)
从运行结果上可以看出,多线程的语言,主线程与子线程之间是完全相互独立的,即使主线程中存在大量的计算逻辑,也不会阻塞子线程的运行;子线程之间也是相互独立的,例如:线程 1 中存在大量计算逻辑并不会影响线程 2 的正在执行。
Java 默认的子线程执行顺序是由 cpu 随机调度的,上述的线程 1 启动后,cpu 就没有马上调度到它。
process.nextTick()
process.nextTick() 是 Node.js 提供的一个异步执行函数,它不是 setTimeout(fn, 0) 的别名,它的效率更高,它的执行顺序要早于 setTimeout 和 setInterval,它是在主逻辑的末尾任务队列调用之前执行。下面通过一段代码进行验证:
/**
* 执行顺序:主线程逻辑 => nextTick => setTimeout
*
*/
console.log(1);
setTimeout(() => console.log(‘setTimeout=> 1’),0);
process.nextTick(() => console.log(‘nextTick=> 1’));
console.log(2);
setTimeout(() => console.log(‘setTimeout=> 2’),0);
process.nextTick(() => {
console.log(‘nextTick=> 2’);
for (let i = 0; i < 10000222200; i++) {} // 一直等待它执行完毕后,才会执行下一个 nextTick() 和之后的任务队列中的回调函数
});
console.log(3);
process.nextTick(() => console.log(‘nextTick=> 3’));
setTimeout(() => console.log(‘setTimeout=> 3’),0);
console.log(4);
setTimeout(() => console.log(‘setTimeout=> 4’),0);
process.nextTick(() => console.log(‘nextTick=> 4’));
console.log(5);
for (let i = 0; i < 10000222200; i++) {} // 一直等待它执行完毕后,才会执行 nextTick 和 setTimeout 的回调。
%20%E3%80%81setTimeout()%E3%80%81setInterval()%20%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/imgs/70262924.png)
从运行结果中我们可以发现,即使 setTimeout 设置的时机要早于 process.nextTick(),但是 process.nextTick() 的执行时机还是要早于 setTimeout,这就证明是了 process.nextTick() 的执行时机是在任务队列调用之前进行执行的。
setInterval()
setInterval() 是一个定时器函数,可按照指定的周期(以毫秒计)来不断调用函数或计算表达式。但是由于 JavaScript 是一个单线程的语言,所以这个定时器的指定的周期回调时间,并不准确;下面通过一段代码来说明一下:
/**
* setInterval 也是要等待主线程执行完毕后,才会进行调用, 如果 timeout 时间一样,就按照 setInterval 设置的顺序进行执行。
* 如果有一个 setInterval 回调函数中有大量的计算,那么线程就阻塞在这个回调函数里,其他的 setInterval 也会等到这个回调执行完毕后才会调用。
*/
console.log(‘main => 1’);
setInterval(() => {
console.log(‘setInterval=> 2 before’);
for (let i = 0; i < 10022222220; i++) {} // 此处会阻塞一段时间,等待计算完毕后才会执行下一个 setInterval 的回调。
console.log(‘setInterval=> 2 after’);
}, 1000);
setInterval(() => {
console.log(‘setInterval=> 1’);
}, 1000);
console.log(‘main => 2’);
for (let i = 0; i < 1002222200; i++) {} // 此处主逻辑会阻塞一段时间进行循环计算,只有主逻辑代码执行完毕后才会调用 setInterval
%20%E3%80%81setTimeout()%E3%80%81setInterval()%20%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/imgs/71471274.png)
上述代码中同时启动了两个 setInterval() 并且它们的回调周期时间都为 1000 毫秒,但是从运行结果中我们可以发现这俩 setInterval() 的回调周期时间远远超出了 1000 毫秒;造成这种情况的主要有两个地方:
第一是在代码的第 17 行,这里有一个很大的循环计算,它的循环时间远远超过了 1000 毫秒,所以这俩 setInterval 只能等主逻辑的代码执行完毕后才能执行。
第二是在代码的第 9 行,在第一个 setInterval 里也有一个很大的循环计算,它的循环时间也超过了 1000 毫秒,所以第二个 setInterval 的回调函数也必须要等待前一个 setInterval 执行完毕后才能进行调用。
由此我们可以得出一个结论:setInterval() 的执行时机是在主逻辑执行完毕之后,它的执行顺序是根据回调周期的时间和设置的顺序进行调用,同一时间只会执行一个 setInterval 的回调函数,只有等待上一个 setInterval 回调函数执行完毕后,才能执行下一个 setInterval 的回调函数。