定时器是我们经常使用的一个异步函数,它的用处十分广泛,比如图片轮播、各种小的动画、延时操作等等;
定时器函数只有两个setTimeout、setInterval,这两个工作原理相同,唯一的区别是:setTimeout只执行一次,setInterval循环执行;
通过以下实例看看对定时器原理掌握程度:
定时器3个实例
首先声明这三个实例输出皆不同,先思考输出结果,以及为何不同
实例一:
console.log('test1')
for(var i=0;i<10;i++){
setTimeout(()=>{console.log(i)},1000);
}
console.log('test2')
实例二(使用了ES6 let关键字):
console.log('test1')
for(let i=0;i<10;i++){
setTimeout(()=>{console.log(i)},1000);
}
console.log('test2')
实例三(使用了ES6 let关键字):
console.log('test1')
for(let i=0;i<10;i++){
setTimeout(()=>{console.log(i)},1000*i);
}
console.log('test2')
结果如下:
实例一:'test1' --> 'test2' --> 同时输出十个10
实例二:'test1' --> 'test2' --> 同时输出0-9数字
实例三:'test1' --> 'test2' --> 每哥1s输出一个数字,数字从0-9
至于原因等会再讲,首先要先明白定时器的工作原理,而定时器的原理正是javascript事件循环模型的体现;其次要掌握闭包;
js运行机制:Event Loop
必须明确一点:javascript是单线程,同一时间只能做一件事;
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
所谓单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个。不妨叫它主线程。但是实际上还存在其他的线程。例如:处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等。这些线程可能存在于JS引擎之内,也可能存在于JS引擎之外,在此我们不做区分。不妨叫它们工作线程。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下(以ajax为例):
解答上述实例
第一个实例:
1.主线程自上向下同步执行,首先输出'test1';
2.遇到for循环后,发现里面是异步函数定时器,就把定时器放到消息队列中,执行了10次,因此消息队列中有10个定时器消息;注意:此时全局变量i=10;
3.将定时器放到消息队列中后,主线程继续执行同步任务;输出'test2';
此时主线程中的同步任务执行完了,主线程上是空的;开始执行消息队列中的消息;
4.由于定时器延时1s,因此1s后消息队列中的消息进入主线程(注意:此时符合条件的有10个定时器消息);
在主线程中执行定时器回调函数时,该回调函数是个闭包,i是全局变量,此时的i=10,因此执行结果是10个10;
第二个实例:
此实例跟第一个实例唯一的区别是 let i 此时i是局部变量;以上四个步骤都是一样的,区别在主线程执行定时器回调函数;此处代码经过babel编译后:
var _loop = function (arg) {
setTimeout(function () {
console.log(arg);
}, 1000);
};
for (var _i = 0; _i < 10; _i++) {
_loop(_i);
}
新定义了一个函数_loop,这个是定时器回调函数的父作用域;
当主线程处理定时器回调函数时,回调函数通过 作用域链 从_loop中获取arg的值;
延伸实例
上述方法使用了ES6中的let关键字,如何使用ES5闭包解决这个问题?
其实,想法就是创建个局部作用域,让定时器的回调函数从局部作用域中读取数值;
for(var i=0;i<10;i++){
(function(arg){
setTimeout(function(){
cosnole.log(arg)
},1000)
})(i)
}
// 在for语句中创建立即执行函数,形成一个局部作用域;
// 定时器的回调函数从局部作用域中读取数值
还有别的方法吗?
可以从定时器的回调函数入手,将这个参数构造成闭包;
function backFun(arg){
return function(){
console.log(arg)
}
}
for(var i=0;i<10;i++){
setTimeout(backFun(i),1000)
}
// 同样是利用闭包创建局部作用域
// 定时器的回调函数 读取的仍是局部作用域中的数值
总结一下,这个题目考察了哪些知识点:
1.闭包
2.作用域链
3.定时器运行的原理(异步执行原理)
4.js运行机制
参考:(感谢以下文档)
[1] javascript: 彻底理解同步、异步、事件循环(Event Loop)
[2] javascript 运行机制详解:在谈Event Loop
[3] 如何使用定时器传递参数
[4] 什么是Event Loop