您的位置 首页 java

10分钟一篇文章教会你Event loop——浏览器和Node

Event Loop 是一个很重要的概念,指的是计算机系统的一种运行机制。

JavaScript语言就采用这种机制,来解决单线程运行带来的一些问题。

想要理解Event Loop,就要从程序的运行模式讲起。运行以后的程序叫做“进程”(process),一般情况下,一个进程一次只能执行一个任务。

如果有很多任务需要执行,不外乎三种解决方法。

以JavaScript语言为例,它是一种单线程语言,所有任务都在一个线程上完成,即采用上面的第一种方法。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现”假死”,因为JavaScript停不下来,也就无法响应用户的行为。

你也许会问,JavaScript为什么是单线程,难道不能实现为 多线程 吗?

这跟历史有关系。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的概念,然而实际上的操作中我们会遇到非常多的问题,让我们无从下手,在这里我给大家介绍一个自己的qun,里面有很多热情的大神,更有实际操作过的老手和老师,我也会不时分享学习的资料和视频(2131 26486),填写并私聊我(落叶)直接领取。

在实际工作中,了解Event loop的意义能帮助你 分析一些异步次序的问题 (当然,随着es7 async和await的流行,这样的机会越来越少了)。除此以外,它还对你 了解浏览器和Node的内部机制 有积极的作用;对于 参加面试 ,被问到一堆异步操作的执行顺序时,也不至于两眼抓瞎。

3. 浏览器上的实现

在JavaScript中,任务被分为Task(又称为MacroTask,宏任务)和MicroTask(微任务)两种。它们分别包含以下内容:

MacroTask : script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering

MicroTask : 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个阶段,它们会按照顺序反复运行,分别如下:

  1. timers:执行setTimeout() 和 setInterval()中到期的 callback

  2. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行

  3. idle, prepare:队列的移动,仅内部使用

  4. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段

  5. check :执行setImmediate的callback

  6. 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)
浏览器输出:
time1
promise1
time2
promise2
Node输出:
time1
time2
promise1
promise2 

在这个例子中,Node的逻辑如下:

最初timer1和timer2就在timers阶段中。开始时首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;

至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、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 timer2
promise1 或者 promise2
timer2 timer1
promise2 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。

综上,关于最关键的顺序,我们要 依据以下几条规则 :

  1. 同一个上下文下,MicroTask会比MacroTask先运行

  2. 然后浏览器按照一个MacroTask任务,所有MicroTask的顺序运行,Node按照六个阶段的顺序运行,并在每个阶段后面都会运行MicroTask队列

同个MicroTask队列下 process.tick() 会优于 Promise

文章来源:智云一二三科技

文章标题:10分钟一篇文章教会你Event loop——浏览器和Node

文章地址:https://www.zhihuclub.com/193513.shtml

关于作者: 智云科技

热门文章

网站地图