浏览器/nodeJS 中的事件环工做原理

众所周知,JS的最大特色之一即是单线程.这意味着JS中若是从上到下执行命令,若是前面的命令花时间太长,则会出现"假死"状态,影响用户体验. 所以在浏览器/nodeJS中,经过webAPI等方式, 将这些长时间的js命令经过异步"分流"到其余的线程(JS自己是单线程,可是浏览器和nodeJS是多线程), 等这些命令执行完成后经过回调函数"返回"JS中. 而这一套机制的实现 就是事件环(eventloop). 下面咱们就来仔细研究一下它的工做原理.html

浏览器中的事件环工做原理

首先 用一张图来展现浏览器中的事件环: 前端

Alt text
浏览器中的事件环

从这张图中咱们能够看到其中有宏任务(MacroTask)和微任务(MicroTask)之分,咱们来讲下这个宏任务与微任务。node

宏任务包括:react

  • setTimeout
  • setInterval 微任务包括:
  • Promise
  • MutaionObserver
  • Object.observe(已废弃:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe)

在单次的迭代中,event loop首先检查Macrotask队列,若是有一个Macrotask等待执行,那么执行该任务。当该任务执行完毕后(或者Macrotask队列为空),event loop继续执行Microtask队列。(V8 中 Microtask 默认是自动运行的)。web

讲了这么多理论, 先来一点代码看看:chrome

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
复制代码

这段代码的顺序如何呢? 将代码放入chrome执行, 咱们能够获得顺序以下: 数据库

Alt text
注意 该图是chrome的结果 不一样浏览器可能呈现不一样结果

那么咱们来分析一下为何是按照这个顺序出现的

咱们先看一看wiki中对宏任务和微任务的定义:编程

"Tasks(宏任务) are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially. Between tasks, the browser may render updates. Getting from a mouse click to an event callback requires scheduling a task, as does parsing HTML, and in the above example, setTimeout.数组

setTimeout waits for a given delay then schedules a new task for its callback. This is why setTimeout is logged after script end, as logging script end is part of the first task, and setTimeout is logged in a separate task. Right, we're almost through this, but I need you to stay strong for this next bit…promise

Microtasks(微任务) are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task. The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed. Microtasks include mutation observer callbacks, and as in the above example, promise callbacks.

Once a promise settles, or if it has already settled, it queues a microtask for its reactionary callbacks. This ensures promise callbacks are async even if the promise has already settled. So calling .then(yey, nay) against a settled promise immediately queues a microtask. This is why promise1 and promise2 are logged after script end, as the currently running script must finish before microtasks are handled. promise1 and promise2 are logged before setTimeout, as microtasks always happen before the next task."

根据以上的描述, 咱们一步一步的分析以前的代码:

step1

程序执行到第一行 直接输出 script start

step2

接下来 setTimeout进入宏任务列表中 以下图所示:

step3

接下来 Promise进入微任务列表中 以下图所示:

step4

而后程序直行至最后一行,输出script End:

step5

而后 先执行微任务中的命令:

step6

then中的部分是直接执行 所以console中显示promise1

step7

因为promise的回调函数中返回'undefined'因而将下一个promise 进入到微任务中.

step8

下图中的 promise then 和promise callback对应的都是第二个then的. 而promise2也在console中显示.

step9

最终结果如step10所示:

step10

看完了这一题 是否是以为事件环也没有想象中那么难呢? 那么在这里你们能够再看看下一题做为思考题. 限于文章篇幅所限,仅提供正确答案供你们参考 ^_^

首先, 咱们来一个html页面:

<div class="outer">
  <div class="inner"></div>
</div>
复制代码

获得以下的一个大方块套小方块的html页面:

若是该html页面的JS以下所示,那么我点击内部的小方块,会获得怎样的结果呢?

// Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener… function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
复制代码

(使用chrome)正确答案:

click

promise

mutate

click

promise

mutate

timeout

timeout

node中的事件环工做原理

事件驱动

Node采用事件驱动的运行方式。在事件驱动的模型当中,每个IO工做被添加到事件队列中,线程循环地处理队列上的工做任务,当执行过程当中遇到来堵塞(读取文件、查询数据库)时,线程不会停下来等待结果,而是留下一个处理结果的回调函数,转而继续执行队列中的下一个任务。这个传递到队列中的回调函数在堵塞任务运行结束后才被线程调用。

Node Async IO 这一套实现开始于Node开始启动的进程,在这个进程中Node会建立一个循环,每次循环运行就是一个Tick周期,每一个Tick周期中会从事件队列查看是否有事件须要处理,若是有就取出事件并执行相关的回调函数。事件队列事件所有执行完毕,node应用就会终止。Node对于堵塞IO的处理在幕后使用线程池来确保工做的执行。Node从池中取得一个线程来执行复杂任务,而不占用主循环线程。这样就防止堵塞IO占用空闲资源。当堵塞任务执行完毕经过添加到事件队列中的回调函数来处理接下来的工做。

固然这么华丽的运行机制就能解决前面说的两个弊端。node基于事件的工做调度能很天然地将主要的调度工做限制到了一个线程,应用能很高效地处理多任务。程序每一时刻也只需管理一个工做中的任务。当必须处理堵塞IO时,经过将这个部分的IO控制权交给池中的线程,能最小地影响到应用处理事件,快速地反应web请求。 固然对机器方便的事情对于写代码的人来讲就须要更当心地划分业务逻辑,咱们须要将工做划分为合理大小的任务来适配事件模型这一套机制。

事件队列调度

Node能够经过传递回调函数将任务添加到事件队列中,这种异步的调度能够经过5种方式来实现这个目标:异步堵塞IO库(db处理、fs处理),Node内置的事件和事件监听器(http、server的一些预约义事件),开发者自定义的事件和监听器、定时器以及Node全局对象process的.nextTick()API。

异步堵塞IO库

其IO库提供的API有Node自带的Module(好比fs)和数据库驱动API,好比mongoose的.save(doc, callback)就是将繁重的数据库Insert操做以及回调函数交给子线程来操做,主线程只负责任务的调度。当MongoDB返回给Node操做结果后,回调函数才开始执行。

Dtree.create(frontData, function (err, dtree) {
      if (err) {
            console.log('Error: createDTree: DB failed to create due to ', err);
            res.send({'success': false, 'err': err});
      } else {
            console.log('Info: createDTree: DB created successfully dtree = ', dtree);
            res.send({'success': true, 'created_id': dtree._id.toHexString()});
      }
});
复制代码

好比这段处理Dtree存储的回调函数只有当事件队列中的接收到来自堵塞IO处理线程的执行完毕才会被执行。

Node内置的事件和事件监听器

Node原生的模块都预约义来一些事件,好比NET模块的一套服务状态事件。当Net中的Socket检测到close就会调用放置在事件循环中的回调函数,下例中就是将sockets数组中删除相应的socket链接对象。

socket.on('close', function(){
  console.log('connection closed');
  var index = sockets.indexOf(socket);
  //服务器端断开相应链接
  sockets.splice(index, 1);
});
复制代码

开发者自定义的事件

Node自身和不少模块都支持开发者自定义事件和处理持戟处理函数,固然既然是自定义,那么触发事件也是显性地须要开发者。在Socket.io编程中就有很好的例子,开发者能够自定义消息事件来处理端对端的交互。

//socket监听自定义的事件消息
socket.on('chatMessage', function(message){
  message.type = 'message';
  message.created = Date.now();
  message.username = socket.request.user.username;
  console.log(message);
  //同时也能够像对方发出事件消息
  io.emit('chatMessage', message);
});
复制代码

计时器(Timers)

Node使用前端一致的Timeout和Interval计时器,他们的区别在Timeout是延时执行,Interval是间隔一段事件执行。值得注意的是这组函数其实不属于JS语言标准,他们只是扩展。在浏览器中,他们属于BOM,即它的确切定义为:window.setTimeout和window.setInterval;与window.alert, window.open等函数处于同一层次。Node把这组函数放置于全局范围中。

除了这两个函数,Node还添加Immediate计时器,setImmediate()函数是没有事件参数的,在事件队列中的当前任务执行结束后执行,而且优先级比Timeout、Interbal高。

计时器的问题在于它在事件循环中并不是精确的执行回调函数。《深刻浅出Node.js》举了一个例子:当经过setTimeout()设定一个任务在10毫秒后执行,可是若是在9毫秒后,有一个任务占用了5毫秒的CPU,再次炖老定时器执行时,事件就已通过期了。

Node全局对象process的.nextTick()API

这个延时执行函数函数是在添加任务到队列的开头,下一次Tick周期开始时就执行,也就是在其余任务前调度。

nextTick的优先级是高于immediate的。而且每轮循环,nextTick中的回调函数所有都会执行完,而Immediate只会执行一个回调函数。这里有得说明每一个Tick过程当中,判断事件循环中是否有事件要处理的观察者。在Node的底层libuv,事件循环是一个典型的生产者/消费者模型。异步IO、网络请求是事件的生产者,回调函数是事件的消费者,而观察者则是在中间将传递过来的事件暂存起来。回调函数的idle观察者在每轮事件循环开始被检查,而check观察者后于idle观察者检查,二者之间被检查的就是IO操做的观察者。

事件驱动与高性能服务器

前面大体介绍了Node的事件驱动模型,事件驱动的实质就是主循环线程+事件触发的方式来运行程序。Node的异步IO成功地使得IO操做与CPU操做分离成为一套高性能平台,既能够像Nginx同样构建服务器平台,也能够处理具体的业务。虽然Node没有Nginx在Web服务器方面那么专业,但不错的性能和更多的使用场景使得在实际开发中可以达到优异的性能。这一切也都归功与异步IO实现的核心——事件循环。在实际的项目中,咱们能够结合不一样工具的优势达到应用的最优性能。

相关文章
相关标签/搜索