看了阮一峰老师的JavaScript 运行机制详解:再谈Event Loop和【朴灵评注】的文章,查阅网上相关资料,把自己对javascript运行模式和EVENT loop的理解整理下,不一定对,日后再看做个回顾。
MDN上有张图很形象,
function f(b){
var a = 12;
return a+b+35;
}
function g(x){
var m = 4;
return f(m*x);
}
g(21);
上面函数调g用形成了一个 frames 的栈。调用g的时候,创建了第一个 frame,包含了 g 的参数和局部变量。当 g 调用 f 的时候,第二个 frame 就被创建、并置于第一个 frame 之上,包含了 f 的参数和局部变量。当f返回时,最上层的 frame 就出栈了(剩下 g 函数调用的 frame)。当g返回的时候,栈就空了。
队列
一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因此而创建的一个初始栈结构)。当栈再次为空的时候,也就意味着消息处理结束。
在浏览器里,当一个事件出现且有一个事件监听器被绑定时,消息会被随时添加。如果没有事件监听器,事件会丢失。所以点击一个附带点击事件处理函数的元素会添加一个消息。其它事件亦然。
绝不阻塞
一个很有趣的事件循环 (event loop) 模型特性在于,Javascript 跟其它语言不同,它永不阻塞。处理 I/O (input/output) 通常由事件或者回调函数进行实现。所以当一个应用正等待 IndexedDB 的查询的返回或者一个 XHR 的请求返回时,它仍然可以处理其它事情例如用户输入。
例外是存在的,如 alert 或者同步 XHR,但避免它们被认为是最佳实践。注意的是,例外的例外也是存在的(但通常是实现错误而非其它原因)。
Event Loop
举例node.js的Event Loop
朴灵的解释
- 【完全不是不同的任务分配给不同的线程。只有磁盘IO操作才用到了线程池(unix)。】
- 【Node中,磁盘I/O的异步操作步骤如下:】
- 【将调用封装成中间对象,交给event loop,然后直接返回】
- 【中间对象会被丢进线程池,等待执行】
- 【执行完成后,会将数据放进事件队列中,形成事件】
- 【循环执行,处理事件。拿到事件的关联函数(callback)和数据,将其执行】
- 【然后下一个事件,继续循环】
使用事件驱动的系统中,必然有非常非常多的事件。如果事件都产生,都要主循环去处理,必然会导致主线程繁忙。那对于应用层的代码而言,肯定有很多不关心的事件(比如只关心点击事件,不关心定时器事件)。这会导致一定浪费。
【事实上,不是所有的事件都放置在一个队列里。】
【不同的事件,放置在不同的队列。】
【当我们没有使用定时器时,则完全不用关心定时器事件这个队列】
【当我们进行定时器调用时,首先会设置一个定时器watcher。事件循环的过程中,会去调用该 watcher,检查它的事件队列上是否产生事件(比对时间的方式)】
【当我们进行磁盘IO的时候,则首先设置一个io watcher,磁盘IO完成后,会在该io watcher的事件队列上添加一个事件。事件循环的过程中从该watcher上处理事件。处理完已有的事件后,处理下一个watcher】
【检查完所有watcher后,进入下一轮检查】
【对某类事件不关心时,则没有相关watcher】
定时器
定时器仅仅是在未来的某个时刻将代码添加到代码队列中,执行时机是不能保证的。代码队列按照先进先出的原则在主进程空闲后将队列中的代码交给主线程运行。
在Javascript中没有任何代码是立刻执行的,带一旦进程空闲则尽快执行。例如,当某个按钮被按下时,事件处理函数会被添加到代码队列中。当接收到ajax响应时,回校函数的代码被添加到队列中。而定时器对队列的工作方式是,当特定的事件过去后将代码加入到队列中。设定一个150ms后执行的定时器不代表代码会在150ms之后执行,而是指代码会在150ms后加入到代码队列中。等到主进程空闲时并且该元素位于队列首位,其中的代码便会立即执行,看上去好像是在精确的时间点上执行了。实际上队列中的所有代码都要等到主进程空闲之后才能执行,而不管他们是怎额添加到队列中去的。
(function () {
console.log('this is the start');
setTimeout(function cb() {
console.log('this is a msg from call back');
});
console.log('this is just a message');
setTimeout(function cb1() {
console.log('this is a msg from call back1');
}, 0);
console.log('this is the end');
var time= new Date();
while(new Date() - time < 2000) {}
})();
//打印顺序如下:
this is the start
this is just a message
this is the end
//2S之后执行settimeout内容,虽然0秒后执行setTimeout内容,但是主线程代码还没执行完,so等主线程空闲的时候再立即执行setTimeout代码
this is a msg from call back
this is a msg from call back1
当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才能将定时器代码添加到代码队列中。
var original = Date.now();
setInterval(function(){
console.log('run interval', Date.now() - original);
var start = Date.now();
while(Date.now() - start < 350) {};
original = Date.now();
}, 200);
var start = Date.now();
while(Date.now() - start < 300) {};
在使用setInterval时:
- 某些间隔会被跳过
- 多个定时器的代码执行之间的间隔可能会比预期的小(当前的setInterval回调正在执行,后一个添加)
参考:http://www.cnblogs.com/dojo-lzz/p/4606448.html
所以在使用setInterval做动画时要注意两个问题:
-
不能使用固定步长作为做动画,一定要使用百分比: 开始值 + (目标值 - 开始值) * (Date.now() - 开始时间)/ 时间区间
-
如果主进程运行时间过长,会出现跳帧的现象。为了避免setInterval的两个缺点,可以使用链式setTimeout():
setTimeout(function(){
//其他处理
setTimeout(arguments.callee, interval);
}, interval);