JavaScript 运行机制--Event Loop详解

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

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

堆、栈、队列

堆(heap)

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

栈(stack)

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

队列(queue)

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

Event Loop

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

浏览器环境下的Event Loop

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

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

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

接下来上代码:异步

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任务……socket

最终咱们获得的输出为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。

相关文章
相关标签/搜索