给初学者的一篇文章:Node.js Event Loop执行机制 (4,5,9面试经常会问)未完。。。待续。。。

464 阅读15分钟

1.Node能够解决什么问题?

Node的首要目标是提供一种简单的,用于创建高性能服务器的开发工具
Web服务器的瓶颈在于并发的用户量(在浏览器端经常会接受到一些客户端的请求,完了转发给我们的服务端),对比Java和Php的实现方式

画张图:(传统的java或者tomcat服务器)


进程:计算机在分配资源时,都是按进程为单位                                                                       线程:线程是进程的一个个的单元(一个进程由很多个线程组成)                                           进程大于线程

图解:比如,我们有一个客户端或者多个客户端,同时访问我们的服务器(常见的服务器有tomcat、iis,这样的服务器的特点是:多线程;服务器的执行空间我们一般都叫做:进程;)当我们客户端发送请求的时候,会请求我们的服务端,那我们的服务端会开一条所谓的线程来去配合,这个线程就相当于一个空间,这个空间会向我们的后台(数据库,常见的mysql数据库,包括其他数据库)发送请求,这样的话,这个服务器会把这个请求转发给mysql去请求我们的数据,那此时这个线程就不通了,你想,如果没有接收到数据库的结果,就卡在这了。那比如再来一个客户端访问我们的服务器,再请求,再来一个线程把这个请求转发给我们的数据库,这种操作一般叫做io操作(操作多区数据库的数据),那这个时候,发现个问题,客户端发送的请求越多,线程就越多,一般情况下线程处理的很快,比如说,数据库请求成功后,把结果返回,那这个线程就关闭了,以前是这样的,但是现在这个线程不会关闭了,现在这个tomcat转变了策略,他会把这个线程进行复用,比如说,默认开20个线程,一个客户端发送请求,服务器就开一个线程,但是这个线程不会被销毁,再有请求的时候,这个线程再出来,它永远复用用一个线程。

但是如果是多线程情况下:

  1. 可能会浪费资源(来一个客户端请求,就开一个线程,浪费一个资源)
  2. 多线程靠的是切换时间片,这个过程中也会浪费资源
  3. 多线程 锁的问题

在传统的java或者tomcat服务器都会有这些多线程的问题,所以node的好处就来了:

画张图:(node)


图解:node的特点:单线程(单线程没有锁的概念,不会同时操作同一个数据),他所谓的单线程并不是说我们应用里面就一条线程,他指的是主线程(主线程是单线程的),也就是说一个应用(进程)里面可能就一条主线程,就是说它里面可能也有很多条线程,比如其他线程,像setTimeout,setTimeout也算是线程,但是setTimeout不在我们的主线程里面,而且setTimeout的好处就是:比如说,我们一个客户端来访问我们的服务端,这时候服务端开一个线程来配合,并且去访问数据库,但是node有个特点:异步非阻塞,也就是说,它并不会等待这个数据库什么时候完成,他只告诉客户端要什么数据,然后它就闲下来了。他不会等着数据库吧这个结果返回给我们客户端,这个时候比如说:再来一个客户端访问服务器,上一个客户端就不管了,然后直接把线程1移到下面,这个时候,如果有很多客户端来访问服务器,服务器只需要访问数据库,不需要等待数据库返回结果,知道数据库开始返回结果了,这个线程1再返回去吧数据返回个客户端。所以node可以扛几千万的并发,但是java只能扛个四五千万,性能上有很大的差距。

Node在处理高并发,I/O密集场景有明显的性能优势

  • 高并发,是指在同一时间并发访问服务器
  • I/O密集指的是文件操作、网络操作、数据库,相对的有CPU密集,CPU密集指的是逻辑处理运算、压缩、解压、加密、解密
Web主要场景就是接收客户端的请求读取静态资源和渲染界面,所以Node非常适合Web应用的开发。

2.Node是什么?

Node.js是一个基于 Chrome V8 引擎的JavaScript运行环境(runtime),Node不是一门语言是让js运行在后端的运行时,并且不包括javascript全集,因为在服务端中不包含DOMBOM,Node也提供了一些新的模块例如http,fs模块等。Node.js 使用了事件驱动非阻塞式 I/O 的模型,使其轻量又高效并且Node.js 的包管理器 npm,是全球最大的开源库生态系统。

3.进程与线程

进程是操作系统分配资源和调度任务的基本单位(一个应用可能就是一个进程),线程是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。最能比喻的就是我们的浏览器,一个浏览器是多进程的。

3.1 画张图:谈谈浏览器


  • 用户界面-包括地址栏、前进/后退按钮、书签菜单等
  • 浏览器引擎-在用户界面和呈现引擎之间传送指令(浏览器的主进程)
  • 渲染引擎,也被称为浏览器内核(浏览器渲染进程)
  • 一个插件对应一个进程(第三方插件进程)
  • GPU提高网页浏览的体验(GPU进程)

由此可见浏览器是多进程的,并且从我们的角度来看我们更加关心浏览器渲染引擎

图解:用户界面,浏览器引擎,浏览器渲染引擎这三个都是进程。在我们写代码的时候最关心的是浏览器的渲染引擎,这个渲染引擎里面又分成了:Networking(请求),Javascript Interperter(JS线程),UI Backend(渲染css的)。Networking(请求),Javascript Interperter(JS线程),UI Backend(渲染css的)这三部分都是线程,我们关心的有两个线程:JS线程和UI线程,这两个线程的关系如下:

线程

  • JS线程  UI线程:这两个线程是同时在浏览器上运行的,但是有个区别:这两个线程是互斥的,目的就是为了保证不产生冲突,因为ui线程是渲染dom,js线程是操作dom,两个线程同时进程会冲突。所以js线程执行,ui线程就不执行;ui线程执行,js线程就不执行。
  •  JS线程执行的时候,ui线程可能再渲染,那渲染的结果放在哪呢?=>这时候,ui线程会把更改放在队列中,当js线程空闲下来,ui线程再继续渲染。

 webworker 多线程 *

  • 他和js主线程不是平级的,主线程可以控制webworker,webworker不能操作dom 不能获取document,window

  写个webworker的小列子:


       



3.2  渲染引擎

渲染引擎内部是多线程的,内部包含两个最为重要的线程ui线程和js线程。这里要特别注意ui线程和js线程是互斥的,因为JS运行结果会影响到ui线程的结果。ui更新会被保存在队列中等到js线程空闲时立即被执行。

3.3  js单线程

javascript在最初设计时设计成了单线程,为什么不是多线程呢?如果多个线程同时操作DOM那岂不会很混乱?这里所谓的单线程指的是主线程是单线程的,所以在Node中主线程依旧是单线程的。

3.4 其他线程

  • 浏览器事件触发线程(用来控制事件循环,存放setTimeout、浏览器事件、ajax的回调函数)
  • 定时触发器线程(setTimeout定时器所在线程)
  • 异步HTTP请求线程(ajax请求线程)
单线程特点是节约了内存,并且不需要在切换执行上下文。而且单线程不需要管锁的问题,这里简单说下锁的概念。例如下课了大家都要去上厕所,厕所就一个,相当于所有人都要访问同一个资源。那么先进去的就要上锁。而对于node来说。下课了就一个人去厕所,所以免除了锁的问题!

4.浏览器中的Event Loop(面试中经常会问到的事件环)


  • 1.所有同步任务都在主线程上执行,形成一个执行栈
  • 2.主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行
  • 4.主线程从任务队列中读取事件,这个过程是循环不断的

整个的这种运行机制又称为Event Loop(事件循环)


图解:如图,事件环分成几部分。js分为两部分,一个叫heap(堆),一个叫stack(栈);对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里面可能会调一些Dom操作,ajax操作和setTimeout定时器。stack(栈)里面的程序先走,走完后再走WebAPIs(异步的方法),WebAPIs(异步的方法)执行后的结果放在callback queue(回调的队列里),也就是当栈里面的程序走完之后,再从任务队列中读取事件放到栈里面去执行,这个过程是循环不断的。

小例子1:


图解代码执行方式:先把console.log(1),console.log(2),console.log(5)放在stack(栈)中执行,然后栈里面可能会调用setTimeout,但是栈里面一定会先一次执行完也就是1,2,5;然后两个setTimeout方法并没有执行,会先移到队列(callback queue)里面去,然后当栈里面都执行完之后,开始从从任务队列中读取事件往栈里面去执行。

小列子2:


图解代码执行方式:先把console.log(1),console.log(2),console.log(5)放在stack(栈)中执行,然后栈里面可能会调用setTimeout,但是栈里面一定会先一次执行完也就是1,2,5;然后两个setTimeout方法并没有执行,会先移到队列(callback queue)里面去,然后当栈里面的1,2,5都执行完之后,开始从从任务队列中读取事件往栈里面去执行,这时候3,4执行,并且6,7放到队列里面,然后3,4执行完后,再从任务队列中读取事件往栈里面去执行。所以顺序是1,2,5,3,4,6,7

如果设置了setTimeout的时间,那就是按setTimeout的成功时间依次执行,如图:这里的顺序是1,2,5,4,7,3,6。也就是只要两个set时间不一样的时候 ,就set时间短的里面走完(包括set里面的回调函数),再走set时间慢的。如果两个set时间一样,就是按顺序走set,走完后再按顺序走set里面的回调。


小列子3:当触发回调函数时,会将回调函数放到队列中。永远都是栈里面执行完后再从任务队列中读取事件往栈里面去执行。



stack(栈)和queue(队列)的区别:

栈方法LIFO(Last In First Out):先进后出(先进的后出),典型的就是函数调用。

例如:


代码走势图:


图解:执行栈里面最先放的是全局作用域(代码执行有一个全局文本的环境),完了再放one。完了one执行再把two放进来,完了two执行再把three放进来,一层叠一层。那么怎么出呢,怎么销毁的呢? => 最先走的肯定是three,因为two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,然后是two出,再是one出。

队列方法FIFO(First In First Out)

(队头)[1,2,3,4](队尾)   进的时候从队尾依次进1,2,3,4  出的时候从对头依次出1,2,3,4



代码执行都是按栈的结果去执行的,但是我们调用完多线程的方法(WebAPIs),这些多线程的方法是放在队列里的,也就是先放到队列里的方法先执行。那什么时候WebAPIs里的方法会再执行呢? => 比如:stack(栈)里面都走完之后,就会依次执行队列里的方法,当栈里面再写个事件,这个事件可能还会调用WebAPIs里的异步方法,那这些异步方法会在再被调用的时候放在队列里,然后这个主线程(也就是stack)从任务队列中读取事件,这个过程是循环不断的。

5.Node系统

node工作和浏览器中的事件环不一样,node有自己的机制。我们先来张图看看node是如何工作的


  • 1.我们写的js代码会交给v8引擎进行处理
  • 2.代码中可能会调用nodeApi,node会交给libuv库处理
  • 3.libuv通过阻塞i/o和多线程实现了异步io
  • 4.通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。

node的特点:异步  非阻塞i/o  node通过LIBUV这个库自己实现的异步,默认的情况下是没有异步的方法的。

图解:我们写的js代码是和v8引擎进行交互的,这个v8引擎会提供一些方法,比如setTimeout属于我们v8引擎的方法,v8引擎里面还会调用nodeAPI(常见的nodeAPI有readFile(文件读取),readFile是异步的,可以读取文件),调用完nodeAPI以后,会把方法交给LIBUV异步库,这个异步库通过BLOCKING(阻塞)和WORKER THREADS(工作线程,这个工作线程是n个的),所以说node底层是多线程的。它是根据BLOCKING OPERATION阻塞的io和WORKER THREADS(多线程)实现了异步io,最后通过EVENT LOOP事件环把结果返回给我们的应用。他会把成功的结果放在EVENT QUEUE(事件队列)里去,同样会调用这个事件队列里的结果,再返回给我们的应用。

6.同步与异步

同步和异步关注的是消息通知机制

  • 同步就是发出调用后,没有得到结果之前,该调用不返回,一旦调用返回,就得到返回值了。 简而言之就是调用者主动等待这个调用的结果
  • 而异步则相反,调用者在发出调用后这个调用就直接返回了,所以没有返回结果。换句话说当一个异步过程调用发出后,调用者不会立刻得到结果,而是调用发出后,被调用者通过状态、通知或回调函数处理这个调用。

7.阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

8.组合

同步异步取决于被调用者,阻塞非阻塞取决于调用者

  • 同步阻塞
  • 异步阻塞
  • 同步非阻塞
  • 异步非阻塞(node就是异步非阻塞)

9.宏任务和微任务(面试经常问)


同步代码先执行 执行是在栈中执行的,然后微任务会先执行,再执行宏任务

任务可分为宏任务和微任务

  • macro-task(宏任务): setTimeout, setInterval, setImmediate, I/O
  • micro-task(微任务): process.nextTick, 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver不兼容的

Promise.then(源码见到Promise就用setTimeout),then方法不应该放到宏任务中(源码中写setTimeout是迫不得已的),在我们真正浏览器的实现里这个then属于微任务的。如图:


图解:先走console.log(1),这里的new Promise()是立即执行的,所以是同步的,由于这个then在console.log(2)后面执行的,所以不是同步,是异步的。那这跟宏任务和微任务有什么关系? => 我们可以加一个setTimeout(宏任务)对比一下,如图:


结论:同步代码先执行  执行是在栈中执行的,然后微任务会先执行,再执行宏任务

MutationObserver小列子:



10.什么场合下应该考虑使用Node框架

当应用程序需要处理大量并发的输入输出,而在向客户端响应之前,应用程序并不需要进行非常复杂的处理。

  • 聊天服务器
  • 电子商务网站