早读课

event-loop

javascript:

  • 同步任务(主线程):存放于执行栈
  • 异步任务:任务队列中(更确切的说是回调队列,queue,先进先出,但是有settimeout的特例)

主线程中执行会产生堆、栈,也同时会调用外部的api,会不断往任务队列中加入事件(click,mouseover…)。一旦执行栈中任务结束,就读取任务队列中,如果发现任务队列中某个异步事件有结果了(进入回调了),就拖到主线程的执行栈中执行。

event-loop是指:程序从主线程执行->调用外部api,在任务队列中放置事件->继续执行主线程->主线程清空/异步任务进入回调,执行任务队列中。(更新,更确切的说是,将task queue里面的函数提升到stack中去执行的这个事件一个一个循环的步骤)
14874674683014

======更新
之前看阮大大的博客,后来被推荐看jsConf上的个视频
讲解的很清楚,这边总结来说是:
setTimeout(fn,delay):

webapis将事件隔delay时间后插入task queue,即使将delay设为0,也只是表示立刻将fn插入task queue(尾部),但是什么时候执行,时间不定,要等当前代码执行完(执行栈清空)以后,即eventloop需要等到stack清空以后再push task(callback)queue中的fn到stack中去执行

对于ajax这些,是等数据返回后,再把回调放入queue中。

render queue跟callback queue一样,只不过有更高的优先级,当stack清空完以后,会首先执行它,
因此如果是同步操作的话,会一直阻塞,让stack一直执行,render queue无法被call到,因此推荐异步,可以在执行间隙,每隔16ms(就会去访问下render queue,把它提上来(前提是stack清空))。

=======更新 end

Nodejs中的event-loop

添加了两个函数process.nextTicksetImmediate

  • process.nextTick: 始终在当前执行栈的尾部添加(如果有多个process.nextTick语句,不管它们是否嵌套,将全部在当前”执行栈”执行。)
  • setImmediate:始终在当前”任务队列”的尾部添加事件,如果嵌套,则下一轮,再下一轮..
  • setTimeout: 指定时间的话,是从当前执行开始计时,到时间就插入到task queue中,在任务队列的尾部添加。

关于setTimeout与setImmediate的执行顺序不定:
官方文档中说是在非I/O context下,两者执行顺序不定,由进程的性能决定,但是在I/o环境中,比如fs的回调中,setImmedite始终在setTimeout之前决定)但是:

Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//在Immediate中有两个函数Im和setTimeout, im的回调A始终在timeout之前,
//在当前执行队列的最后,而B则是在下一轮event-loop,执行队列尾部
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2

结论:

>我们由此得到了process.nextTick和setImmediate的一个重要区别:
多个process.nextTick语句总是在当前"执行栈"一次执行完,
多个setImmediate可能则需要多次loop才能执行完。

因此,当递归调用process.nextTick的话,会被⚠️要求用immediate替代,因为这样会一直在执行栈中,不会去检查任务队列。

补充知识

楼主看了下node里面介绍,关于process.nextTick,因为上面说它有一堆问题(什么递归调用会导致线程阻塞啊blahblah,那干嘛还要有这个api)。主要的原因是,比如在进入event-loop之前去做一些其他操作(有点点像finally),比如throw error以后,

1
2
3
4
5
function apiCall (arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback,
new TypeError('argument should be string'));
}

要执行error回调之前(event-loop)想执行其他操作,比如一些unneeded资源的回收,或者请求重传等等。
还有的情况是诸如保证变量/函数定义、执行的先后顺序,
看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event'); //1
//2
//process.nextTick(function () {
// this.emit('event');
//}.bind(this));
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
console.log('an event occurred!');//3
});

用new执行完MyEmitter构造函数后,按理说要emit,但是这个时候其实并没有执行到下面(下面的on事件的注册也在执行栈中,不过还没调用到),因此emit要触发时还未定义。为了解决这个问题,应该将1处更改为2,那么,emit的触发会先暂缓,下面继续执行,event被注册,3处的回调被放入callback queue,然后这一步执行完后(也就是当前栈中的所有都已经执行完,只剩下刚刚nextTick中的emit),此时,事件已注册,因此触发,栈清空,event-loop开始,回调被挪入stack中执行。

课后小测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(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');
})();
//

答案:

14874869698329