平时的工作中,也许你会经常用到 setTimeout 这个方法,可是你真的了解 setTimeout 吗?本文想通过总结 setTimeout 的用法,顺便来探索 javascript 里面的事件执行机制。
1、
- setTimeout(code,millisec)
setTimeout 函数接受两个参数,第一个参数 code 是将要推迟执行的函数名或者一段代码,第二个参数 millisec 是推迟执行的毫秒数。
例如:
- setTimeout('console.log(2)', 100);
- setTimeout(function() {
- console.log(2)
- },
- 100);
如果直接在 setTimeout 中直接执行代码, 需要以字符串的形式去写,引擎内部会将字符串转为可执行的代码
2、再来一些简单些的代码
- console.log(1);
- setTimeout('console.log(2)',1000);
- console.log(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);
这个时候的输入顺序是怎样的呢?这里先埋个伏笔,因为我们是以 setTimeout 来聊 Event Loop
什么是 Event Loop 呢?
因为 javascript 是单线程的,所谓的单线程是指在 JS 引擎中负责解释和执行 JavaScript 代码的线程只有一个,可以叫它为主线程。
除了主线程,还存在其他的线程。例如:处理 AJAX 请求的线程、处理 DOM 事件的线程、定时器线程、读写文件的线程 (例如在 Node.js 中) 等等。我们以 setTimeout 为例,当在代码中调用 setTimeout()方法时,注册的延时方法会交由浏览器内核其他模块(以 webkit 为例,是 webcore 模块)处理,当延时方法到达触发条件,即到达设置的延时时间时,这一延时方法被添加至任务队列里。这一过程由浏览器内核其他模块处理,与执行引擎主线程独立,执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行,这一过程是一个不断循环的过程,称为事件循环模型。
Javascript 执行引擎的主线程运行的时候,产生堆(heap)和栈(stack)。程序中代码依次进入栈中等待执行,当调用 setTimeout() 方法时,即图中右侧 WebAPIs 方法时,浏览器内核相应模块开始延时方法的处理,当延时方法到达触发条件时,方法被添加到用于回调的任务队列,只有执行引擎栈中的代码执行完毕,主线程才会去读取任务队列,依次执行那些满足触发条件的回调函数。
在上图中的 callback queue 中指的是 "任务队列",也可以理解为消息的队列,"消息" 我们可以简单理解为是:注册异步任务时添加的回调函数。
例如:
- setTimeout(function() {
- console.log('hello');
- },
- 100);
其中里面的 function(){console.log('hello')} 就是一个消息,任务队列里面保存的就是这些回调函数
我们以一段代码的运行来进行理解,代码如下:
- console.log('start');
- //Timer1
- setTimeout(function() {
- console.log('hello');
- },
- 200);
- //Timer2
- setTimeout(function() {
- console.log('world');
- },
- 100);
- console.log('end');
代码运行的 gif 图如下:
我们分步骤来进行这个过程解答
1、 js 执行引擎开始执行上述代码时,会先讲一个 main() 方法加入执行栈。首先第一个 console.log('start') 入栈,console.log 方法是一个 webkit 内核支持的普通方法,而不是前面图中 WebAPIs 涉及的方法,所以这里 log('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 里面的回调函数加入了事件队列
回到我们文章之初的那倒题:
- 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);
如果理解了上面的内容,那么这道题理解起来就比较容易了。
首先是打印出 1,然后是 10000 个 4,那么 Time1、Time2、Time3 是顺序是如何的呢?
在这个代码中,for 循环比较耗时,在 Time1 和 Timer 加入到执行队列中后,主线程依然还在执行 for 循环中的代码,处于阻塞状态。队列中的 Time1 和 Time2 并不会得以执行。当 for 循环结束,这时才将 Time3 交由 Timer 模块去管理,清空执行栈。虽然在这里 Time3 的延迟时间最短,但是加入任务队列后还是会排在 Time1 和 Time2 的后面,所以此时按顺序执行任务队列中的代码,依次打印 2、3、5。
所以执行结果为:
理解 js 的事件循环在平时的工作中还是挺有用的,它可以让我们清楚的知道事件的执行顺序,知道事件的走向,才能更好的驾驭 Javascript。本文是对事件循环的一个小小总结,更多的干货,可以看看下面的参考文档。本文有误之处,欢迎指出
参看文档:
来源: http://www.cnblogs.com/xianyulaodi/p/6414805.html