Node.js究竟是什么

接触前端也有一段时间了,逐渐开始接触Node.js,刚刚接触Node.js的时候一直都觉得Node.js就是JavaScript,当对Node.js有必定的了解以后,其实并否则二者之间有关系,其中的关系又不是必然的,对Node.js进行的一些了解,对其进行一些概述,本篇文章并无对Node.jsAPI进行讲解,而是可以更加的明白Node.js是什么。前端

到底什么是Node.js

先看一下Node.js官网中是如何形容Node.js的,打开官网看到的第一句话就是Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.(Node.js是一个JavaScript运行时创建在Chrome的V8 JavaScript引擎。)在上面这段话中最重要的一点就是运行时node

到底什么是运行时呢?其实在笔者看来运行时就是程序在运行时所须要的组件,能够将其想象成为是一种编程语言的运行环境。然而这个运行环境包含了代码运行时所须要的解释器和底层操做系统的支持等。程序员

文章开头也说过Node.jsJavaScript之间有关系,可是其关系也不是必然,到这里大概也就有点眉目了。对于任何语言来讲,其中最终要的就是其解释器如何去处理这些编程语言。Node.js的底层是使用C++实现的,然而语法则是遵循ECMAScript规范,其实彻底能够把其实现换乘一种新的编程语言,更换语言的同时也就意味着其解释器发生了翻天覆地的变化。编程

Node.js为何要选择JavaScript

到了这里可能有些疑问,编程语言和解释器有关系,那么为何要选择了JavaScript然而不是其余的语言呢?Node.js做者(Ryan Dahl)说,在创造Node.js的时,其目的是为了实现高性能的Web服务器,其看重的并非JavaScript这门语言。可是他须要的事一种编程语言来实现其想法,这种编程语言不能带有任何的IO功能,而且须要良好的支持事件机制。说到这里感受就是在说JavaScript这门语言(感受就是天命之选,O(∩_∩)O哈哈~)。首先JavaScript彻底知足上述的两个条件,然而就顺其天然的JavaScript就成了Node.js的主导者。segmentfault

Runtime

上面一直提到的就是RuntimeRuntime是什么?运行时刻是指一个程序在运行(cc或者在被执行)的状态。也就是说,当你打开一个程序使它在电脑上运行的时候,那个程序就是处于运行时刻。在一些编程语言中,把某些能够重用的程序或者实例打包或者重建成为运行库。这些实例能够在它们运行的时候被连接或者被任何程序调用(节选自百度百科)。后端

其实对于开发者来讲根本就不用去考虑其背后究竟是怎样实现的,咱们站在开发的角度来想想,对于某一种语言的Runtime表示开发者能够在Runtime上运行某种语言所编写的代码,若是把这个概念扩大一下说的话,Chorome也是一个JavaScript运行时依赖于背后的JavaScript引擎来运行JavaScript代码而已。浏览器

其对应的Runtime能够对其编程语言进行一些拓展,好比在Node.js中的fs、Buffer就是其对ECMAScript的拓展,Runtime并不包含整个ECMAScript中的所有特性。反过来说,就算一个特性没有体如今标准里,而大多数的运行时都支持它,也能够变成实际上的规范。经过上述所说咱们能够理解到对于任何语言来说咱们无需对其底层的实现,全部的东西都依赖于其运行时的实现而已,运行时环境对其支持状况才能表现出其语言的特性。缓存

一样的一段代码可能在浏览器端能够顺利执行,可是放到Node.js中不必定能够顺利执行,反之也是同样的,这样的就足能够说明上述问题了。服务器

Node.js内部机制

Node.js中有几个很重要的关键词单线程,非阻塞异步IO,在笔者刚刚接触Node.js的时候,这几个词常常听到,有些懵懵懂懂不是太能理解。为了更好的了解其内部机制那么针对这些东西进行说明。网络

回调函数

为何要说回调函数呢?对Node.js模块有必定了解的话Node.js中模块都是依赖于回调函数的,那么什么是回调函数呢?

回调函数就是一个经过函数指针调用的函数。若是你把函数的指针(地址)做为参数传递给另外一个函数,当这个指针被用来调用其所指向的函数时,咱们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。(节选自百度百科)。

上面说了一堆套话,其实回调函数就是讲一个函数做为参数传给另外一个函数做为参数,而且该函数能够被执行。回调方法和主线程处于同一个线程,假设主线程发起了一个底层的系统调用,操做系统会执行这个系统调用,当这个系统调用完成以后则会再回到主进程去执行后续的方法。

Node.js中在操做过程当中可能会有一个比较耗时的IO操做,当IO操做有了返回结果以后才会继续向下执行,其中在进行IO操做时就形成了代码的阻塞,在Node.js最初设计的时候已经考虑到了这一点,因此提出了异步函数回调函数的方式,也能实现高并发的处理。对于前端来说Ajax就是一个异步回调函数,当发起请求时若是有后续代码会先向下继续执行,而不会等待期请求结果。

回调函数机制:

  1. 定义一个回调函数;
  2. 提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者;
  3. 当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。
同步/异步

有关于同步/异步也搜索了一些文献,可是都是简简单单归纳一下,没有细致的说明。所谓同步和异步其描述的事进程和线程的调用方式。由于Node.js的单线程,所以同个时间只能处理同个任务,全部任务都须要排队,前一个任务执行完,才能继续执行下一个任务,可是,若是前一个任务的执行时间很长,好比文件的读取操做或网络请求,后一个任务就不得不等着,拿文件的读取操做来讲,当用户向后台读取大量的文件时,不得不等到全部数据都读取完毕才能进行下一步操做,后续程序只能在那里干等着,颇有可能形成响应超时。所以,Node.js在设计的时候,就已经考虑到这个问题,主线程能够彻底不用等待文件的读取完毕,能够先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取有告终果后,再回过头执行挂起的任务,所以,任务就能够分为同步任务和异步任务。

  1. 同步任务:同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当咱们打开网站时,网站的渲染过程,好比元素的渲染,其实就是一个同步任务
  2. 异步任务:异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务能够执行了,该任务才会进入主线程,当咱们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务

上述所说同步调用指的是进程/线程发起调用后,一直等待调用结果返回后才会继续向下执行,可是对于Node.js来讲虽然也是这样,可是并不表明的CPU在这段时间内也会一直等待,操做系统多半会切换到另外一个进程/线程上等调用返回结果后在切回原有进程/线程。然而异步则偏偏相反,当发起异步调用时,进程/线程会继续向下执行,当调用返回结果后经过某种技术手段通知其调用者已经有其结果。

咱们一直都在说的一句话就是JavaScript是一门异步语言,可是对于ECMAScript而言并无对异步有明确的规范,实际上是其解释器(Node.js或浏览器)的runtime的其余线程来实现的,这些并非JavaScript这门语言自己的功能。

对于异步请参考:浅析JavaScript异步

阻塞/非阻塞

笔者在没有了解阻塞/非阻塞以前一直觉得同步/异步与阻塞/非阻塞之间是没有区别的,然而现实就是这么的打脸,阻塞/非阻塞和同步/异步彻底就是两组概念,他们之间没有任何的必然关系。不少人大概和我同样同步=阻塞异步=非阻塞,这种概念是彻底不对的。

在了解阻塞与非阻塞以前首先要了解一下什么是IO操做,IO操做实际上是内存与外部设备之间复制数据的过程。

在阻塞的状况,是会一直等待直到write彻底部的数据再返回。这点行为上与读操做有所不一样,究其缘由主要是读数据的时候,一般刚开始咱们并不知道要读的数据的长度,而是在数据的头部设置了一个长度,在读完指定长度的头部后,才知道整个要读的数据长度。若是一开始就贸然设置一个要读的数据长度,而后像阻塞的write那样去等读完,则极可能会形成死循环;而对于write,因为须要写的长度是已知的,因此能够一直再写,直到写完。不过问题是write是可能被打断形成write一次只write一部分数据,因此write的过程仍是须要考虑循环write, 只不过多数状况下一次write调用就可能成功。

非阻塞写的状况,是采用能够写多少就写多少的策略。与读不同的地方在于,有多少读多少是由网络发送端是否有数据传输到本地内核缓存为准。可是对于能够写多少是由本地的网络堵塞状况为标准的,在网络阻塞严重的时候,网络层没有足够的内存来进行写操做,这时候就会出现写不成功的状况,阻塞状况下会尽量(有可能被中断)等待到数据所有发送完毕, 对于非阻塞的状况就是一次写多少算多少,没有中断的状况下也仍是会出现write到一部分的状况。

其实用一句话来讲讲的话,同步调用会形成进程的IO阻塞,而异步不会形成调用进程的IO阻塞。

单线程与多线程

Node.js并无提供多进程的支持,这表明在程序中所编写的代码只能运行在当前进程中,用于运行代码的事件也是单线程进行的。开发者没法在一个独立进程中增长新的线程吗,可是能够派生出多个进程来达到必行完成任务。

进程

进程是指在操做系统中正在运行的一个应用程序

线程

线程是指进程内独立执行某个任务的一个单元。线程本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。

对于Node.js,若是说JavaScript的函数式编程方式使得其异步编程的思想对程序员展示得更天然,那么它背后的功臣Libuv,则为异步编程的实现提供了可能。

1141038-20180307094514784-1056185595.png

上图中从左往右分为两部分,一部分是与Network I/O相关的请求,而另一部分是由File I/O, DNS Ops以及User code组成的请求。

从图中能够看出,对于Network I/O和以File I/O为表明的另外一类请求,异步处理的底层支撑机制是彻底不同的。 对于Network I/O相关的请求, 根据OS平台不一样,分别使用Linux上的epollOSXBSDOS上的kqueueSunOS上的event ports以及Windows上的IOCP机制。 而对于File I/O为表明的请求,则使用thread pool。利用thread pool的方式实现异步请求处理,在各种OS上都能得到很好的支持。Libuv团队为何要选择thread pool的机制。基本上缘由不外乎编码和维护复杂度过高、可支持的API太少且质量堪忧、技术支持较弱,而用thread pool则很好地避开了这些问题。

Node.js的异步调用时由Libuv来支持的,以readFile为例的话,读取文件的系统调用是由Libuv来完成的,Node.js只负责调用Libuv所提供的接口就能够了,等结果返回后在执行对应的回调方法。

并行与并发

自从Node.js出现后,JavaScript开始涉及后端领域,由于其出色的并发模型,被不少企业用来处理高并发请求。

与并发被同时说起到的还有并行,那么并行与并发有有什么区别?并行指在同一时间点同时执行,并发是指在同一时间片断同时执行,上面已将解释进程与线程,此时就可理解,进程之间相互独立,可实现并行,但线程不能够,多线程只能并发执行,实际仍是顺执行,只是在同一时间片断,假似同时执行,CPU能够按时间切片执行,单核CPU同一个时刻只支持一个线程执行任务,多线程并发事实上就是多个线程排队申请调用CPUCPU处理任务速度很是快,因此看上去多个线程任务说并发处理。

并发指的是一个CPU在不一样线程来回跳,而后你会看到两个线程抢夺CPU资源因此两个线程输出执行的顺序不固定。

Node.js中的并发任务处理:

  1. 每一个Node.js进程只有一个主线程在执行程序代码,造成一个执行栈。
  2. 主线程以外,还维护了一个"事件队列"。当用户的网络请求或者其它的异步操做到来时,Node都会把它放到事件栈之中,此时并不会当即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
  3. 主线程代码执行完毕完成后,而后经过事件循环,也就是事件循环机制,开始到事件栈的开头取出第一个事件,从线程池中分配一个线程去执行这个事件。

接下来继续取出第二个事件,再从线程池中分配一个线程去执行,而后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中全部事件都执行完了。

此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。

咱们所看到的Node.js单线程只是一个JavaScript主线程,本质上的异步操做仍是由线程池完成的,Node.js将全部的阻塞操做都交给了内部的线程池去实现,自己只负责不断的往返调度,并无进行真正的I/O操做,从而实现异步非阻塞I/O,这即是Node.js单线程和事件驱动的精髓之处了。

总结

读完本篇文章应该对Node.js有了一个简单的认识,其中提到的EventLoop在本文章并无进行解释,有时间会对其进一步说明。Node.js完成了它提供高度可伸缩服务器的目标。它使用了Google的一个很是快速的JavaScript引擎,即V8引擎。它使用一个事件驱动设计来保持代码最小且易于阅读。全部这些因素促成了Node.js的理想目标,即编写一个高度可伸缩的解决方案变得比较容易,其Node.js对于高并发的处理也有很好的支持,总之Node.js的强大之处还有不少仍然须要慢慢摸索。

文章中概念较多,你们能够做理解,最后感谢你们用这么长时间来阅读这篇文章,文章中若是有什么差错请在评论处提出,我会尽快作出改正。

相关文章
相关标签/搜索