网站结构优化包括哪些/怎么做链接推广产品
我记得有一次面试的时候,面试官问道,setTimeout 的用法,现在就来总结一下。
setTimeout 的基本用法
setTimeout(fn,delay)
一般情况下,setTimeout
函数接收两个参数,第一个参数fn
是将要推迟执行的函数名或者是一段代码,第二个参数 delay
是推迟执行的毫秒数。
例如:
setTimeout('console.log(1)',1000);
setTimeout(function(){console.log(2)},1000);
如果直接在 setTimeout
中直接执行代码,需要用字符串的形式去写,引擎内部会将字符串转为可执行的代码。
2、再来一些简单些的代码
console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);
依次输出的是 // 1 3 2
3、代码升级版
console.log(1);setTimeout(function(){console.log(2);
},300);setTimeout(function(){console.log(3)
},400);for (var i = 0;i<10000;i++) {console.log(4);
}
setTimeout(function(){console.log(5);
},100);
这个答案我们在后面揭晓。接下来我们讲一下 EventLoop。
Event Loop 简介
我们知道,js 是单线程的,所谓的单线程是指 JS 引擎中负责解释和执行 js 代码的线程只有一个,可以叫它 主线程。
除了主线程,还存在其他的线程,例如:处理 axios 和 ajax 请求的线程,处理 DOM 时间的线程,定时器的线程,node.js 中读写文件的线程。我们以 setTimeout
为例,当在代码中调用 setTimeout
方法时,注册的延时方法会交由 浏览器的内核其他模块(以webkit为例,是 webcore 模块)处理,当延时方法到达触发条件,即到达设置的延时时间时,这一延时的方法会被添加到任务队列中。这一过程由浏览器内核其他模块处理,与执行引擎主线程独立,执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行,这一过程是一个不断循环的过程,成为事件循环模型。
上面这张图中 heap
是堆,stack
是栈。js 执行引擎的主线程运行的时候,会产生堆和栈。程序中代码依次进入到栈中等待执行,当调用 setTimeout
方法的时候,即图中右边的 webAPIs
方法的时候,浏览器内核想应模块开始延时方法的处理,当延时方法到达触发条件时,方法会被添加到用于回调的任务队列,只有执行引擎栈中的代码执行完毕,主线程才会去读取任务队列,依次执行那些满足触发条件的回调函数。
在上图中的callback queue中指的是 “任务队列”,也可以理解为消息的队列,“消息“我们可以简单理解为是:注册异步任务时添加的回调函数。
理解 js 代码的执行
用一段代码的运行来进行理解,代码如下:
console.log('start');//Timer1
setTimeout(function(){console.log('hello');
},200);//Timer2
setTimeout(function(){console.log('world');
},100);console.log('end');
代码运行的 gif 图如下:
我们分步骤来进行代码的解析:
1、js 执行引擎最开始执行上述代码的时候,会将一个 mian() 方法加入到执行栈中。首先 console.log('start')
进入到栈中, console.log()
方法是一个 webkit 内核支持的普通方法,而不是前面图中 webAPIs 中的方法,所以在这里 start 立即出栈被引擎执行了。
2、引擎的方法继续往下,将 setTimeout(callback,200)
添加到执行栈。setTimeout()
方法属于事件循环模型webAPIs 中的方法,引擎在将 setTimeout()
方法出栈执行时,将演示执行的函数交给了相应的模块,即右方的 timer 模块来处理。
3、主线程继续向下执行,紧接着将第二个定时器也交给 Timer 模块,然后执行到第二个 console.log()
,控制台打印 end
。
4、执行完毕后清空执行栈。但是并没有结束,在主线程执行的同时, timer 模块会检查其中的异步代码,一旦满足触发条件,就会将它添加到任务队列中。Timer2延迟100ms,所以会早于Timer1被添加到队列排头。而主线程此时处于空闲状态,所以会检查任务队列是否有待执行的任务。此时会将Timer2回调中的console.log()执行,控制台打印’world’,然后执行栈空闲后继续检查任务队列,将Timer1的代码压入执行栈中执行,控制台打印’hello’,清空执行栈,此时任务队列为空,执行结束,程序处理完毕,main()方法也出栈。
5、在这里再次强调一下,不是setTimeout加入了事件队列,而是setTimeout里面的回调函数加入了事件队列
setTimeout问题的解答
回到文章最初的那道题目:
console.log(1);
//Time1
setTimeout(function(){console.log(2);
},300);
//Time2
setTimeout(function(){console.log(3)
},400);for (var i = 0;i<10000;i++) {console.log(4);
}
//Time3
setTimeout(function(){console.log(5);
},100);
输出:
那么Time1、Time2、Time3是顺序是如何的呢?为什么是按照进入队列的方式进行输出的呢?
修改一下代码:将循环的次数减少到 100
console.log(1);
//Time1
setTimeout(function(){console.log(2);
},300);
//Time2
setTimeout(function(){console.log(3)
},400);for (var i = 0;i<100;i++) {console.log(4);
}
//Time3
setTimeout(function(){console.log(5);
},100);
输出的结果是:
为什么循环次数减少了, setTimeout 的结果却改变了呢?
由此我们先猜测:
如果setTimeout加入队列的阻塞时间大于两个setTimeout执行的间隔时间,那么先加入任务队列的先执行,尽管它里面设置的时间比另一个setTimeout的要大。
我们通过代码来理解一下:
console.log(1);
//Time1
setTimeout(function(){console.log(2);
},300);
//Time2
setTimeout(function(){console.log(3)
},400);
var start=new Date();
for (var i = 0;i<1500;i++) {console.log(4);
}
var end=new Date();
console.log('阻塞耗时:'+Number(end-start)+'毫秒');
//Time3
setTimeout(function(){console.log(5);
},200);
结果如下:
阻塞的时间是 240毫秒,所以,大于任意两个setTimeout 的间隔(最大为200),所以按照谁先进入队列先输出谁的规则进行输出,所以结果是 2,3,5。
下面代码:
setTimeout(function(){console.log(2);
},400);var start=new Date();
for (var i = 0;i<500;i++) {console.log('这里只是模拟一个耗时操作');
};
var end=new Date();
console.log('阻塞耗时:'+Number(end-start)+'毫秒');//Time1
setTimeout(function(){console.log(3)
},300);
98毫秒 < 100 毫秒。因为阻塞的耗时小于Time1和Time2的执行间隔时间100毫秒;因此,setTimeout谁用的时间短,先输出谁。
通过上面的代码说明,我们得到了这样一个结论:
如果setTimeout加入队列的阻塞时间大于两个setTimeout执行的间隔时间,那么先加入任务队列的先执行,尽管它里面设置的时间可能比另一个setTimeout的要大。
setTimeout 的第三个参数
setTimeout(fn, delay) 常见的写法就是两个参数,一个是要执行的函数,一个是延迟时间,多余的参数什么意思呢?
var timeoutID = scope.setTimeout(function[, delay, param1, param2, ...]);
var timeoutID = scope.setTimeout(function[, delay]);
var timeoutID = scope.setTimeout(code[, delay]);
这里的 scope 默认是 window。
一共有3种用法:
第二种用法,就是最常见的用法;
第三种用法,第一个参数是一个字符串,执行的时候解析为 js 语句再执行。这个用法已经不推荐了,完全可以用第二种代替。
第一种用法,就是在第二种之上加了更多的参数,这些参数会在第一个参数函数 fn 执行的时候作为参数传递,即 fn(param1, param2, param3) 这样。
test 函数没有返回值
function test2(value) {value = value || 'default 2';console.log(value);
}setTimeout(test2, 1000, 2.1); // Timer1
setTimeout(test2(), 1000, 2.2); // Timer2
setTimeout(test2(2.3), 1000, 2.31); // Timer3
上面代码如何输出呢?
结果为:
于是 Timer1 意思就是 1s 以后执行 test2(2.1),结果就是1s 以后打印 2.1。===我是分割线===Timer2 ,立即执行 test2(),打印 default 2,由于 test2 函数并没有返回值,1s 以后也不会有任何函数被推入消息队列,多余的参数也没有意义。===我是分割线===Timer3 里 test2 带了一个参数2.3,由于 test2 函数并没有返回值,立即打印2.3,1s 以后啥都没有。
test 函数有返回值
function test3(value) {value = value || 'default 2';console.log(value);return test3;
}setTimeout(test3, 1000, 2.1); // Timer1
setTimeout(test3(), 1000, 2.2); // Timer2
setTimeout(test3(2.3), 1000, 2.31); // Timer3
代码输出为:
解析:
Timer1 比较好理解,1s 以后执行 test3,并传入参数 3.1,实际执行的是 test3(3.1),所以1s 以后打印3.1;执行以后返回的 test3 没有被任何变量接收。===我是分割线===Timer2 立即执行 test3(),立即打印 default 3,并将返回值 test3 推入消息队列,1s 以后执行 test3(3.2),打印 3.2;===我是分割线===Timer3 立即执行 test3(3.3),立即打印 3.3,并将返回值 test3 推入消息队列,1s 以后执行 test3(3.31),打印 3.31;
总结
本文对 setTimeout 和 Event Loop 进行了一个简单的说明。