Event Loop 是一个很重要的概念,指的是计算机系统的一种运行机制。前端
JavaScript语言就采用这种机制,来解决单线程运行带来的一些问题。node
想要理解Event Loop,就要从程序的运行模式讲起。运行之后的程序叫作“进程”(process),通常状况下,一个进程一次只能执行一个任务。面试
若是有不少任务须要执行,不外乎三种解决方法。promise
(1)排队。由于一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。浏览器
(2)新建进程。使用fork命令,为每一个任务新建一个进程。多线程
(3)新建线程。由于进程太耗费资源,因此现在的程序每每容许一个进程包含多个线程,由线程去完成任务。(进程和线程的详细解释,请看这里。)异步
以JavaScript语言为例,它是一种单线程语言,全部任务都在一个线程上完成,即采用上面的第一种方法。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现”假死”,由于JavaScript停不下来,也就没法响应用户的行为。socket
你也许会问,JavaScript为何是单线程,难道不能实现为多线程吗?async
这跟历史有关系。JavaScript从诞生起就是单线程。缘由大概是不想让浏览器变得太复杂,由于多线程须要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来讲,这就太复杂了。后来就约定俗成,JavaScript为一种单线程语言。(Worker API能够实现多线程,可是JavaScript自己始终是单线程的。)函数
若是某个任务很耗时,好比涉及不少I/O(输入/输出)操做,那么线程的运行大概是下面的样子。
上图的绿色部分是程序的运行时间,红色部分是等待时间。能够看到,因为I/O操做很慢,因此这个线程的大部分运行时间都在空等I/O操做的返回结果。这种运行方式称为”同步模式”(synchronous I/O)或”堵塞模式”(blocking I/O)。
若是采用多线程,同时运行多个任务,那极可能就是下面这样。
上图代表,多线程不只占用多倍的系统资源,也闲置多倍的资源,这显然不合理。
Event Loop就是为了解决这个问题而提出的。Wikipedia这样定义:
“Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
简单说,就是在程序中设置两个线程:一个负责程序自己的运行,称为”主线程”;另外一个负责主线程与其余进程(主要是各类I/O操做)的通讯,被称为”Event Loop线程”(能够译为”消息线程”)。
上图主线程的绿色部分,仍是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,而后接着日后运行,因此不存在红色的等待时间。等到I/O程序完成操做,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。
能够看到,因为多出了橙色的空闲时间,因此主线程得以运行更多的任务,这就提升了效率。这种运行方式称为”异步模式“(asynchronous I/O)或”非堵塞模式”(non-blocking mode)。
这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也所以使它具有了其余语言不具有的优点。若是部署得好,JavaScript程序是不会出现堵塞的,这就是为何node.js平台能够用不多的资源,应付大流量访问的缘由。
在实际工做中,了解Event loop的意义能帮助你分析一些异步次序的问题(固然,随着es7 async和await的流行,这样的机会愈来愈少了)。除此之外,它还对你了解浏览器和Node的内部机制有积极的做用;对于参加面试,被问到一堆异步操做的执行顺序时,也不至于两眼抓瞎。
3. 浏览器上的实现
在JavaScript中,任务被分为Task(又称为MacroTask,宏任务)和MicroTask(微任务)两种。它们分别包含如下内容:
MacroTask: script(总体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI renderingMicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver
须要注意的一点是:在同一个上下文中,总的执行顺序为同步代码—>microTask—>macroTask[6]。这一块咱们在下文中会讲。
浏览器中,一个事件循环里有不少个来自不一样任务源的任务队列(task queues),每个任务队列里的任务是严格按照先进先出的顺序执行的。可是,由于浏览器本身调度的关系,不一样任务队列的任务的执行顺序是不肯定的。
具体来讲,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),若是不为空则会一次性执行完全部microtask。而后再进入下一个循环去task队列中取下一个task执行,以此类推。
注意:图中橙色的MacroTask任务队列也应该是在不断被切换着的。
本段大批量引用了《什么是浏览器的事件循环(Event Loop)》的相关内容,想看更加详细的描述能够自行取用。
4. Node上的实现
nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别以下:
timers:执行setTimeout() 和 setInterval()中到期的callback。
I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
idle, prepare:队列的移动,仅内部使用
poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
check:执行setImmediate的callback
close callbacks:执行close事件的callback,例如socket.on("close",func)
不一样于浏览器的是,在每一个阶段完成后,而不是MacroTask任务完成后,microTask队列就会被执行。这就致使了一样的代码在不一样的上下文环境下会出现不一样的结果。咱们在下文中会探讨。
另外须要注意的是,若是在timers阶段执行时建立了setImmediate则会在此轮循环的check阶段执行,若是在timers阶段建立了setTimeout,因为timers已取出完毕,则会进入下轮循环,check阶段建立timers任务同理。
5. 示例
5.1 浏览器与Node执行顺序的区别
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0)浏览器输出:time1promise1time2promise2Node输出:time1time2promise1promise2
在这个例子中,Node的逻辑以下:
最初timer1和timer2就在timers阶段中。开始时首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,一样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段以前,执行microtask队列的全部任务,依次打印promise一、promise2。
而浏览器则由于两个setTimeout做为两个MacroTask, 因此先输出timer1, promise1,再输出timer2,promise2。
更加详细的信息能够查阅《深刻理解js事件循环机制(Node.js篇)》
为了证实咱们的理论,把代码改为下面的样子:
setImmediate(() => { console.log('timer1') Promise.resolve().then(function () { console.log('promise1') })})setTimeout(() => { console.log('timer2') Promise.resolve().then(function () { console.log('promise2') })}, 0)Node输出:timer1 timer2promise1 或者 promise2timer2 timer1promise2 promise1
按理说
setTimeout(fn,0)
应该比
setImmediate(fn)
快,应该只有第二种结果,为何会出现两种结果呢?
这是由于Node 作不到0毫秒,最少也须要1毫秒。实际执行的时候,进入事件循环之后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的情况。若是没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。
另外,若是已通过了Timer阶段,那么setImmediate会比setTimeout更快,例如:
const fs = require('fs');fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2));});
上面代码会先进入 I/O callbacks 阶段,而后是 check 阶段,最后才是 timers 阶段。所以,setImmediate才会早于setTimeout执行。
具体能够看《Node 定时器详解》。
5.2 不一样异步任务执行的快慢
setTimeout(() => console.log(1));setImmediate(() => console.log(2));Promise.resolve().then(() => console.log(3));process.nextTick(() => console.log(4));输出结果:4 3 1 2或者4 3 1 2
由于咱们上文说过microTask会优于macroTask运行,因此先输出下面两个,而在Node中process.nextTick比Promise更加优先[3],因此4在3前。而根据咱们以前所说的Node没有绝对意义上的0ms,因此1,2的顺序不固定。
5.3 MicroTask队列与MacroTask队列
setTimeout(function () { console.log(1); },0); console.log(2); process.nextTick(() => { console.log(3); }); new Promise(function (resolve, rejected) { console.log(4); resolve() }).then(res=>{ console.log(5); }) setImmediate(function () { console.log(6) }) console.log('end');Node输出:2 4 end 5 1 6
这个例子来源于《JavaScript中的执行机制》。Promise的代码是同步代码,then和catch才是异步的,因此4要同步输出,而后Promise的then位于microTask中,优于其余位于macroTask队列中的任务,因此5会优于1,6输出,而Timer优于Check阶段,因此1,6。
综上,关于最关键的顺序,咱们要依据如下几条规则:
同一个上下文下,MicroTask会比MacroTask先运行
而后浏览器按照一个MacroTask任务,全部MicroTask的顺序运行,Node按照六个阶段的顺序运行,并在每一个阶段后面都会运行MicroTask队列
同个MicroTask队列下process.tick()会优于Promise
对前端感兴趣的,想要进行学习的朋友能够加我qq裙:213126486 邀请码:落叶,一块儿讨论进步~