JavaScript(简称JS)是前端的首要研究语言,要想真正理解JavaScript就绕不开他的运行机制--Event Loop(事件环)前端
JS是一门单线程的语言,异步操做是实际应用中的重要的一部分,关于异步操做参考个人另外一篇文章js异步发展历史与Promise原理分析 这里再也不赘述。node
堆(heap)是指程序运行时申请的动态内存,在JS运行时用来存放对象。web
栈(stack)遵循的原则是“先进后出”,JS种的基本数据类型与指向对象的地址存放在栈内存中,此外还有一块栈内存用来执行JS主线程--执行栈(execution context stack),此文章中的栈只考虑执行栈。ajax
队列(queue)遵循的原则是“先进先出”,JS中除了主线程以外还存在一个“任务队列”(其实有两个,后面再详细说明)。vim
JS的单线程也就是说全部的任务都须要按照必定的规则顺序排队执行,这个规则就是咱们要说明的Event Loop事件环。Event Loop在不一样的运行环境下有着不一样的方式。promise
先上图(转自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)浏览器
从事件环中不难看出当咱们调用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;
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
node中的时间循环与浏览器的不太同样,如图:
每个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时,
node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,
event loop会转入下一下阶段。
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语句(无论它们是否嵌套),将所有在当前"执行栈"执行。
两者很是类似,可是两者区别取决于他们何时被调用.
其两者的调用顺序取决于当前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。