
之前看到了一段鬼打墙一般的代码
console.log("1");
window.setTimeout(()=>{console.log("3");},0);
console.log("2");
凭借着我C语言计算机二级的底子,我机智的认为,三行代码会在控制台输出:
1
3
2
但是并没有,控制台输出的是:
1
2
3
好奇怪,代码明明是逐行执行的,但是结果却像跳过了 setTimeout 为0的代码。
明明都已经是0秒之后执行了,为什么它却没有按时执行呢?所以这个“0秒之后”,肯定不是代码逐行执行的时间间隔。
window.setTimeout(()=>{console.log("3");},0);
这行代码等待的0秒,是其它的。最后我也没有猜出来是什么
后来我又看到了类似的“鬼打墙”代码:
let i = 0;
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i);
},0);
};
再次凭借着我C语言计算机二级的底子,我机智的认为,三行代码会在控制台输出:
0 1 2 3 4 5
但是现实再次扇了我一巴掌,和上面的 1 2 3 类似, setTimeout 为0的代码像是被跳过了。控制台的输出结果是: 6 6 6 6 6 6
所以是怎样一种神秘的力量,使得循环内部的语句,在循环结束之后才会执行呢?
我大胆猜测,JS肯定不是逐行执行的!!!
一切问题,在于JS的执行机制。
在查找了资料之后,我明白了两件事情:
- JS是单线程,JS同时只能执行一个任务,其他任务都必须在后面排队等待。
- JS是单线程,但JS引擎不是只有一个线程。JS引擎有多个线程,单个JS脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。
所以,我大胆猜测,window.setTimeout(()=>{console.log("3");},0);内的console.log("3");其实是不在主线程上运行的,所以才会出现,就算语句的字面理解是“0秒之后”执行,但其实语句是在主线程运行结束之后才执行的。
而相关资料也是这个意思。
程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。 JS主线程上执行的,是同步任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务则是没有进入主线程,进入任务队列的任务。异步任务不会被JS 引擎挂起,而是由JS引擎决定任务执行的时机。这样才不会出现由于单线程而导致一个任务阻塞后续任务的进行。
JS引擎在任务队列(task queue)中存放各类当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。)
JS程序会从主线程开始执行所有的同步任务,等到同步任务全部执行完,才会开始执行任务队列里面的异步任务。如果满足异步任务的执行条件,那么异步任务就会重新进入主线程,成为同步任务开始执行。等到第一个符合条件的异步任务执行结束,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
在程序执行的过程中,JS引擎会不断检查,只要同步任务执行结束,JS引擎就会去检查那些挂起来的异步任务是否可以进入主线程。这种循环检查的机制,就叫做事件循环(Event Loop)。维基百科 对事件循环的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
异步操作的模式主要有三种有:
- 回调函数
- 事件监听("onclick")
- 发布/订阅
而window.setTimeout(()=>{console.log("3");},0);就是一个回调函数,简单理解就是,window.setTimeout(,0);语句被加入了主线程,但是由于它声明了**“我要等0秒(在主线程结束之后)再执行”**,所以console.log("3");才会在主进程console.log("1");和console.log("2");执行结束后执行。
那么 6 6 6 6 6 6 的鬼打墙又要怎么解释呢?
let i = 0;
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i);
},0);
};
我们可以这么理解:首先,for循环的代码块被放进了主线程,里面的代码包括:
i = 0;
setTimeout(,0);
i++;
if (i>=6) break;
所以每次执行循环的时候,window.setTimeout(,0);语句都会说**“我要等0秒(在主线程结束之后)再执行”**。所以主线程(代码块内),执行了六次 i++;,直到 i>=6,循环结束,主线程才正式结束。
主线程结束之后,**“我要等0秒(在主线程结束之后)再执行”**的window.setTimeout(,0);语句才会挨个执行。
问题来了,主线程结束之后,console.log("i");(回调函数),才会开始调用主线程内的 i,因为i是全局变量,而这个时候的 i 却已经是6了。
所以我们永远都不会得到和循环一起变化的i么?
不,我们其实可以,这样用就行:
let i = 0;
for(i = 0; i<6; i++){
let j = i;
setTimeout(()=>{
console.log(j);
},0);
};
变量 j 保存了 i 的值,这样在console.log("j");回调函调 j 的时候,就可以得到保留下来的 i 值。
而这样的操作则是不可以的:
let i = 0;
for(i = 0; i<6; i++){
setTimeout(()=>{
let j = i;
console.log(j);
},0);
};
我们的 j 在 setTimeout 执行时,只能获得 i = 6 的值。
另外ES6也提供了另一种方式:
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(j);
},0);
};
因为let创建的变量 i 的作用域是for循环函数,所以console.log("i");回调函调 i 的时候,就可以得到保留下来的 i 值。
或者也可以在每次循环时,将 i 作为值传入:
let i = 0;
for( i = 0; i<6; i++){
function fn(j){
setTimeout(()=>{
console.log(j);
},0);}
fn(i);
};