接触前端有一段时间了,逐渐开始接触Node.js。刚开始接触Node.js的时候,一直以为Node.js就是JavaScript。在对Node.js有了一定的了解之后,其实两者之间并不是真的有关系,也不是必然存在关系。对Node.js的一些了解和一些概述。本文不讲解Node.js的API,但可以更好的理解什么是Node.js。什么是Node.js?我们来看看Node.js官网是如何描述Node.js的。打开官网看到的第一句话是,Node.js?是一个基于Chrome的V8JavaScript引擎构建的JavaScript运行时。(Node.js是基于Chrome的V8JavaScript引擎构建的JavaScript运行时。)上面这段话中最重要的一点是运行时。究竟什么是运行时?其实在笔者看来,runtime就是程序在运行时所需要的组件,可以想象成一种编程语言的运行环境。但是,此运行时环境包括代码运行所需的解释器和底层操作系统支持。文章开头我也说了Node.js和JavaScript之间是有关系的,但关系不是必然的,这里大概是一点点蛛丝马迹。对于任何语言,最终的事情是它的解释器如何处理这些编程语言。Node.js底层使用C++实现,但语法遵循ECMAScript规范。事实上,完全可以将其实现转移到一种新的编程语言中。改变语言也意味着解释器发生了翻天覆地的变化。.为什么Node.js选择JavaScript?这里可能会有一些疑问。编程语言与解释器有关,为什么选择JavaScript而不是其他语言呢?Node.js的作者(RyanDahl)说,在创建Node.js时,它的目的是实现一个高性能的Web服务器,它的重点不在JavaScript语言上。但他需要的是一种编程语言来实现他的想法。这种编程语言不能有任何IO功能,需要很好的支持事件机制。说到这里,感觉像是在说JavaScript这门语言(感觉是命运的选择,O(∩_∩)O哈哈~)。首先,JavaScript完全满足以上两个条件,但JavaScript顺其自然成为了Node.js的领头羊。上面已经说了Runtime就是Runtime,什么是Runtime?运行时间是指程序运行的状态(cc或正在执行)。也就是说,当您打开一个程序以在您的计算机上运行它时,该程序正在运行。在某些编程语言中,一些可重用的程序或实例被打包或重建为运行时库。这些实例在运行时可以被任何程序链接或调用(摘自百度百科)。其实对于开发者来说,根本不需要考虑它背后是如何实现的。我们从发展的角度来考虑。某种语言的Runtime是指开发者可以在Runtime上运行某种语言编写的运行时。如果将这个概念展开,Chorome也是一个JavaScript运行时,它依赖于背后的JavaScript引擎来运行JavaScript代码。其对应的Runtime可以对其编程语言进行一些扩展。比如Node.js中的fs和Buffer就是它对ECMAScript的扩展。运行时并不包括整个ECMAScript中的所有特性。反之,即使一个特性没有体现在标准中,大多数运行时都支持它,它可以成为事实上的规范。通过以上我们可以了解到,对于任何一种语言,我们都不需要去实现它的底层,一切都取决于它的运行时实现,运行时环境对它的支持才能体现出它的语言特性。同样一段代码,在浏览器端可能执行的很流畅,但在Node.js中可能执行不流畅,反之亦然。这足以说明上述问题。Node.js的内部机制Node.js中有几个重要的关键词:单线程、非阻塞异步IO。刚接触Node.js的时候经常听到这句话,有点懵,能力也不强。理解。为了更好的理解它的内部机制,接下来对这些东西进行解释。回调函数为什么说回调函数呢?如果你对Node.js模块有一定的了解,Node.js中的模块依赖回调函数,那么什么是回调函数呢?回调函数是通过函数指针调用的函数。如果你把一个函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用它所指向的函数时,我们就说这是一个回调函数。回调函数不是由函数的实现者直接调用,而是在特定事件或条件发生时被另一方调用,用于响应事件或条件。(摘自百度百科)。上面说了很多套话。回调函数其实就是把一个函数作为参数传递给另一个函数作为参数,这个函数就可以执行了。回调方法和主线程在同一个线程中。假设主线程发起底层系统调用,操作系统会执行系统调用。当系统调用完成后,会回到主进程去执行后续的方法。在Node.js中,在运行过程中可能会有耗时的IO操作。当IO操作有返回结果时,会继续向下执行。当执行IO操作时,代码会被阻塞。在Node.js中.js的初始设计就已经考虑到这一点,所以提出了加异步函数和回调函数的方法,同样可以实现高并发处理。对于前端来说,Ajax是一个异步回调函数。发起请求时,如果有后续代码,则先继续向下执行,不等待请求结果。回调函数机制:定义一个回调函数;提供函数实现的一方在初始化时将回调函数的函数指针注册给调用者;当特定事件或条件发生时,调用者使用函数指针调用回调函数来响应事件进行处理。Synchronous/asynchronous关于synchronous/asynchronous也搜索了一些文档,不过都是简单概括,没有详细解释。所谓同步方法和异步方法描述的是进程和线程的调用方法。由于Node.js的单线程,只能同时处理同一个任务,所有的任务都需要排队,上一个任务执行完才能执行下一个任务。但是,如果前面的任务执行时间很长,比如文件读取操作或者网络请求,后面的任务就得等待。以文件读取操作为例,当用户向后台读取大量文件时,必须等到所有数据都读取完毕后才能继续操作。下一步,后续程序只能在那里等待,可能会导致响应超时。所以在设计Node.js的时候,就已经考虑到了这个问题。主线程不需要等待文件被读取。读取结果后,返回并执行挂起的任务。因此,任务可以分为同步任务和异步任务。同步任务:同步任务是指在主线程上排队等待执行的任务。只有完成了上一个任务,才能执行下一个任务。当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步Task异步任务:异步任务是指不进入主线程而是进入任务队列的任务。只有当任务队列通知主线程有异步任务可以执行时,任务才会进入主线程。当我们打开网站时,像图片和音乐的加载其实是一个异步任务。上面说的同步调用是指进程/线程发起调用后,等待调用结果返回后再继续向下执行。但是,对于Node.js来说,也是一样的。这样,并不意味着CPU也会在这段时间内等待。操作系统很可能会切换到另一个进程/线程,并在调用返回结果后切换回原来的进程/线程。然而,异步恰恰相反。当发起异步调用时,进程/线程会继续向下执行,当调用返回结果时,会通过一些技术手段通知调用者已经得到结果。我们一直在说的是JavaScript是一种异步语言,但是对于ECMAScript并没有明确的异步规范。实际上,它是由其解释器(Node.js或浏览器)运行时的其他线程实现的。是的,这些不是JavaScript语言本身的功能。异步可以参考:JavaScript异步阻塞/非阻塞浅析之前不理解阻塞/非阻塞,一直以为同步/异步和阻塞/非阻塞没有什么区别,但是现实就是这么一记耳光,阻塞/非阻塞阻塞和同步/异步完全是两套概念,它们之间没有必然的关系。很多人大概和我一样,同步=阻塞,异步=非阻塞,这个概念是完全错误的。在了解阻塞和非阻塞之前,我们首先要了解什么是IO操作。IO操作其实就是内存和外部设备之间拷贝数据的过程。在阻塞的情况下,它会等到所有数据都写入后才返回。此行为不同于读取操作。主要原因是我们在读取数据的时候,通常一开始并不知道要读取的数据的长度,而是在数据的头部设置一个长度。读取完指定长度后才知道要读取的整个数据的长度。如果一开始就贸然设置一个数据长度读取,然后像阻塞写入一样等待读取完成,很可能造成死循环;而对于write,由于要写入的长度是已知的,所以可以一直重复。写到写完为止。但是问题是写入可能会被打断,导致写入一次只能写入部分数据,所以写入过程还是需要考虑写入周期,但大多数情况下,一次写入调用可能会成功。在非阻塞写的情况下,采用能写多少就写多少的策略。与读取的区别在于,读取的次数取决于是否有数据从网络发送方传输到本地内核缓存。但是,能写多少是由本地网络拥塞情况决定的。当网络拥塞严重时,网络层没有足够的内存来执行写操作。此时写入失败。有可能(可能被中断)等到所有数据发送完毕。 对于非阻塞的情况,就是一次写多少。即使没有中断,也会有一部分写入。其实一句话,同步调用会造成进程IO阻塞,而异步调用不会造成调用进程IO阻塞。单线程和多线程Node.js不提供多进程支持,也就是说程序中编写的代码只能在当前进程中运行,用于运行代码的事件也是单线程的。开发人员不能在一个独立的进程中添加新的线程,但是他们可以派生多个进程来实现任务的必要完成。进程进程是指运行在操作系统中的应用程序线程。线程是指进程内独立执行某项任务的单元。线程本身基本上不拥有系统资源,只有少数运行中必不可少的资源(如程序计数器、一组寄存器和堆栈)。对于Node.js来说,如果说JavaScript的函数式编程方式让它的异步编程思想对程序员来说更加自然,那么其背后的功臣Libuv则为异步编程的实现提供了可能。上图从左到右分为两部分,一部分是与NetworkI/O相关的请求,另一部分是由FileI/O、DNSOps和Usercode组成的请求。从图中可以看出,对于NetworkI/O和另一种以FileI/O为代表的请求,异步处理的底层支持机制是完全不同的。对于NetworkI/O相关的请求,根据不同的OS平台,Linux使用epoll,OSX和BSDOS使用kqueue,SunOS使用eventports,Windows使用IOCP机制。对于以文件I/O为代表的请求,使用线程池。使用线程池实现异步请求处理,在各种OS上都能得到很好的支持。为什么Libuv团队会选择线程池机制。归根结底是编码和维护复杂度太高,支持的API太少质量堪忧,技术支持薄弱,但是使用线程池可以很好的避免这些问题。libuv支持Node.js的异步调用。以readFile为例,读取文件的系统调用是由Libuv完成的。Node.js只负责调用Libuv提供的接口。等待结果返回。然后执行相应的回调方法。并行与并发自Node.js出现以来,JavaScript就涉足后端领域。由于其优秀的并发模型,已经被很多企业用于处理高并发请求。并行也和并发同时提到,那么并行和并发有什么区别呢?并行是指在同一时间点同时执行。并发是指在同一时间段同时执行。上面已经解释了进程和线程。这时候就可以理解为进程之间是相互独立的,可以实现并行,而线程则不行。多线程只能并发执行,但实际上还是顺序执行,只不过是在同一个时间段,好像是同时执行,CPU是可以按照时间片来执行的。单核CPU只支持一个线程同时执行任务。多线程并发其实就是多线程。排队应用程序调用CPU,CPU处理任务的速度非常快,看起来是多线程任务并发处理的。并发指的是一个CPU在不同的线程中来回跳转,然后你会看到有两个线程在抢CPU资源,所以这两个线程输出执行的顺序是不固定的。Node.js中的并发任务处理:每个Node.js进程只有一个主线程执行程序代码,形成一个执行栈。除了主线程之外,还维护了一个“事件队列”。当用户的网络请求或其他异步操作到来时,Node会将其放入事件栈中。这个时候不会马上执行,代码也不会阻塞。会一直往下走,直到主线程代码执行完毕。完全的。主线程代码执行完后,事件循环,即事件循环机制,开始从事件栈的开头取第一个事件,并从线程池中分配一个线程来执行这个事件。接下来继续取出第二个事件,然后从线程池中分配一个线程去执行,然后是第三个,第四个。主线程不断检查事件队列中是否还有未执行的事件,直到事件队列中的所有事件都执行完毕。此后,每当有新事件加入事件队列时,都会通知主线程按顺序取出,交给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的强大之处还有很多,需要慢慢去发掘。文章中有很多概念,大家可以看懂。最后,感谢您花这么长时间阅读这篇文章。文中如有错误,请在评论中提出,我会第一时间改正。
