2

JavaScript(简称JS)是前端的首要研究语言,要想真正理解JavaScript就绕不开他的运行机制--Event Loop(事件环)

JS是一门单线程的语言,异步操作是实际应用中的重要的一部分,关于异步操作参考我的另一篇文章js异步发展历史与Promise原理分析 这里不再赘述。

堆、栈、队列

堆(heap)

堆(heap)是指程序运行时申请的动态内存,在JS运行时用来存放对象。

栈(stack)

栈(stack)遵循的原则是“先进后出”,JS种的基本数据类型与指向对象的地址存放在栈内存中,此外还有一块栈内存用来执行JS主线程--执行栈(execution context stack),此文章中的栈只考虑执行栈。

队列(queue)

队列(queue)遵循的原则是“先进先出”,JS中除了主线程之外还存在一个“任务队列”(其实有两个,后面再详细说明)。

Event Loop

JS的单线程也就是说所有的任务都需要按照一定的规则顺序排队执行,这个规则就是我们要说明的Event Loop事件环。Event Loop在不同的运行环境下有着不同的方式。

浏览器环境下的Event Loop

先上图(转自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)

  • 当主线程运行的时候,JS会产生堆和栈(执行栈)
  • 主线程中调用的webaip所产生的异步操作(dom事件、ajax回调、定时器等)只要产生结果,就把这个回调塞进“任务队列”中等待执行。
  • 当主线程中的同步任务执行完毕,系统就会依次读取“任务队列”中的任务,将任务放进执行栈中执行。
  • 执行任务时可能还会产生新的异步操作,会产生新的循环,整个过程是循环不断的。

从事件环中不难看出当我们调用setTimeout并设定一个确定的时间,而这个任务的实际执行时间可能会由于主线程中的任务没有执行完而大于我们设定的时间,导致定时器不准确,也是连续调用setTimeout与调用setInterval会产生不同效果的原因(此处就不再展开,有时间我会单独写一篇文章)。

接下来上代码:

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3)
    setTimeout(function(){
        console.log(6);
    })
},0)
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
},0)
console.log(5)

代码中的setTimeout的时间给得0,相当于4ms,也有可能大于4ms(不重要)。我们要注意的是代码输出的顺序。我们把任务以其输出的数字命名。
先执行的一定是同步代码,先输出1,2,5,而3任务,4任务这时会依次进入“任务队列中”。同步代码执行完毕,队列中的3会进入执行栈执行,4到了队列的最前端,3执行完后,内部的setTimeout将6的任务放入队列尾部。开始执行4任务……

最终我们得到的输出为1,2,5,3,4,6,7。

宏任务与微任务

任务队列中的所有任务都是会乖乖排队的吗?答案是否定的,任务也是有区别的,总是有任务会有一些特权(比如插队),就是任务中的vip--微任务(micro-task),那些没有特权的--宏任务(macro-task)。
我们看一段代码:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise')
    })
})
setTimeout(function(){
    console.log(3);
})

按照“队列理论”,结果应该为1,2,3,promise。可是实际结果事与愿违输出的是1,2,promise,3。

明明是3先进入的队列 ,为什么promise会排在前面输出?这是因为promise有特权是微任务,当主线程任务执行完毕微任务会排在宏任务前面先去执行,不管是不是后来的。

换句话说,就是任务队列实际上有两个,一个是宏任务队列,一个是微任务队列,当主线程执行完毕,如果微任务队列中有微任务,则会先进入执行栈,当微任务队列没有任务时,才会执行宏任务的队列。

微任务包括: 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver, MessageChannel;

宏任务包括:setTimeout, setInterval, setImmediate, I/O;

Node环境下的Event Loop

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

node中的时间循环与浏览器的不太一样,如图:

  • timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
  • I/O callbacks 阶段: 执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行。

每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时,
node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,
event loop会转入下一下阶段。

process.nextTick

process.nextTick方法不在上面的事件环中,我们可以把它理解为微任务,它的执行时机是当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行。上代码:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

代码可以看出,不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

setTimeout 和 setImmediate

二者非常相似,但是二者区别取决于他们什么时候被调用.

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行

其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的。

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

这是因为后一个事件进入的时候,事件环可能处于不同的阶段导致结果的不确定。当我们给了事件环确定的上下文,事件的先后就能确定了。

var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
$ node timeout_vs_immediate.js
immediate
timeout

这是因为因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout。


标记层叠样式球
609 声望442 粉丝