TOP

一个定时器相关的题目引发的思考

  定时器是我们经常使用的一个异步函数,它的用处十分广泛,比如图片轮播、各种小的动画、延时操作等等;

  定时器函数只有两个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

posted @ 2017-07-26 17:38  RocketV2  阅读(186)  评论(0编辑  收藏  举报