当前位置: 首页 > 后端技术 > Node.js

《大前端进阶 Node.js》系列必知必问

时间:2023-04-03 19:20:13 Node.js

前言Node.js的核心基础,水怪认为你一定要掌握~每篇文章都希望你能有所收获。本文重点分析Node.js的核心架构和基础。希望你看完后能有这些收获:Node.js架构中各层的含义和关系Node.js是如何与底层操作系统交互的?利用好事件驱动的优势,以及实现方式很多前端初学者,尤其是大学生,第一个遇到的技术瓶颈就是今天要说的Node.js。懂了,有些基础知识没有掌握,比如编译原理。PS:前端小伙伴也要注意计算机基础知识。工作时间越长,体会应该越深~很多看似复杂的东西,其实要回到最底层,就是电脑。比如现在很流行的跨端框架,其核心其实就是AST抽象语法树的转换~好了,进入正题,就请大家来一睹Node.js的神龙吧~架构我相信只要你是前端,都能或多或少的说出你对Node.js的一些理解和看法。我们先来看看浏览器和Node.js的对比。毕竟很多前端初学者可能并没有接触过Node,只是在浏览器中运行项目而已。左图是浏览器的简单架构。我们平时写的前端项目无非就是三个部分。HTML和CSS交给WebKit引擎处理。经过一系列的变换,它们最终显示在我们的屏幕上。之前看过Chrome团队的SteveKobes的分享,从底层分析浏览器的一个渲染过程。以后找时间跟大家分享。JavaScript交给V8引擎处理和解析。本文暂且不谈引擎。再往下到中间层,Chrome中中间层的能力是有限的,因为它受限于浏览器。比如我们要在浏览器中操作一些本地文件,在早期是非常困难的,但是随着HTML5的普及,一些功能已经可以实现了,但是和Node中间层的能力相比,还是差很多。我们去掉左图中红色的部分,其实就是一个简单的Node架构。在Node中,我们可以随意操作文件,甚至可以构建各种服务。尽管Node不处理UI层,但它与浏览器兼容。相同的机制和原理在中间层运行,并且在中间层具有更强大的功能。顺着这个思路,我们再想一想,如果我们也把WebKit引擎也抽出来,再加入Node,是不是可以不用浏览器开发带有UI处理的Node项目呢?想必你已经知道Weird要说什么了,Electron其实就是这么干的,也不是什么特别神奇的东西~所以,简单直观的说,Node是脱离了浏览器,但仍然是基于ChromeV8引擎运行的一个JavaScript环境。从官网的介绍也可以看出,其轻量、高效、事件驱动、非阻塞I/O是Node.js的几个非常重要的特性。下面分析一下Node的单线程是如何做到高并发的,如何充分利用服务器资源。上面的Node架构图比较简单,我们来看一个比较完整的。基础设施大致可以分为以下三层。上层是Node标准库。其实简单理解为JavaScript代码。您可以在编写代码时直接调用相关API。Node提供了很多强大的API供我们实现。在实践中更深入地使用它。举个很简单的例子,我们可以用Node写一个定时脚本,定时给女朋友发邮件,把你想说的话推送给她。女朋友高兴的时候,你也学技术了,完美~中间层Nodebindings(c++实现),这一层简单来说就是红娘,牵线搭桥,让JavaScript兄弟和下层的一帮妹子交流,节点这么强,这一层起着非常重要的作用。下层,Node.js运行时的关键,是东西!下面一一说~V8,可以简单粗略的概括为业界最好的JavaScrpt引擎。尽管有尝试使用V8替代方案,例如node-chakracore项目和spidernode项目,但Node.js仍然默认使用V8引擎。C-ares,C语言实现的异步DNS请求库;http_parser、OpenSSL、zlib等,提供一些其他的基础能力。libuv是一个高性能、事件驱动的I/O库,并提供跨平台(如Windows、Linux)的API。它强制执行异步、事件驱动的编程风格,其核心工作是提供基于I/O和其他事件通知的事件循环和回调函数。并且还提供了一些核心工具,比如定时器、非阻塞网络支持、异步文件系统访问、子进程等,层层叠叠。Node写情书的底层操作,这里参考《深入浅出 Node.js》一书中的例子进行讲解。假设我们需要打开一个本地的txt文件给女朋友写一封情书。代码可以这样写:letfs=require('fs');fs.open('./LoveLetter.txt',"w",function(err,fd){//海誓山盟,永不言败分离(彼此,永不分离)});fs.open()的作用是根据指定的路径和参数打开一个文件,返回一个文件描述符。让我们转到lib/fs.js并查看底层源代码:asyncfunctionopen(path,flags,mode){mode=modeNum(mode,0o666);path=getPathFromURL(路径);验证路径(路径);validateUint32(mode,'mode');returnnewFileHandle(awaitbinding.openFileHandle(pathModule.toNamespacedPath(path),stringToFlags(flags),mode,kUsePromises));}JavaScript代码调用C++核心模块来执行较低级别的操作。调用过程可以表述为从JavaScript调用Node.js标准库,再从标准库调用C++模块。这是Node中最常见的调用方式。同时,libuv还提供了*UNIX和Windows平台的实现,赋予了Node.js跨平台的能力。就这样,情书搞定了,你们的关系也改善了,从此过上了幸福的生活~毕竟是单线程的,需要解释一下,我正在写结构分析的文章基于Node.js的高并发掩码秒杀系统的实现哦~第一个问题解决了,我们来看第二个。既然Node是单线程的,那么它是如何应对高并发场景的呢?其实Node在很多地方都是多线程的,除了JavaScript是单线程的。从上面写情书的例子可以看出,Node的I/O操作其实是交给了libuv,libuv提供了完整的线程池实现。因此,所有的I/O操作都可以并行化,除了用户的JavaScript代码不能并行执行。对线程池之类不熟悉的朋友,老老实实告诉怪怪,你在大学里忙着给女朋友写情书吗?!!实际上,操作系统中对I/O的处理方式只有两种,即阻塞和非阻塞。阻塞I/O是指在调用之后,需要等待所有操作完成后,调用才会结束,导致CPU等待I/O结束,处理能力没有得到充分利用。比如你现在是一个CPU,现在你要做两件事,第一件事是给在外面逛街的女朋友发信息问你会不会回来吃饭(因为你要做饭,哈哈哈),第二件事就是打扫房间。同步I/O的方法:给女朋友发消息,然后在线等一个小时后女朋友终于回消息。然后,你又去打扫房间,等你女朋友回来,她看到你为什么这么久才开始打扫房间,然后大家就往你头上扣~~异步I/O方法:发消息给你女朋友,然后直接开始打扫房间,等女朋友回话,房间已经打扫好了,饭也做好了,岂不是很愉快?~回到操作系统,操作系统提供了一种非阻塞的I/O方法,调用后会立即返回,然后CPU可以处理其他事务。但是,由于I/O未完成,因此仅立即返回调用的状态。为了得到最终的结果,应用程序需要进行足够多的调用来判断操作是否完成,即轮询。目前常见的轮询技术有几种:read是最原始的一种,通过反复调用读取最终结果。在得到结果之前,CPU会一直消耗在等待中。Select在read的基础上进行了改进。它判断文件描述符上的事件状态。当用户进程调用select时,整个进程会被阻塞。同时,内核会“监控”所有select负责的socket。当任何套接字中的数据准备就绪时,select将返回。这时用户进程调用read操作将数据从内核拷贝到用户进程。select以及后续的poll和epoll也称为I/O多路复用。select使用长度为1024的数组来存储状态,因此最多可以同时检查1024个文件描述符。pollpoll采用链表的方式,避免了数组长度的限制,提高了性能。epoll是Linux下最高效的I/O事件通知机制。如果进入轮询时没有检测到I/O事件,它会休眠直到有事件发生将其唤醒,不会浪费CPU。kqueue实现类似于epoll,只存在于BSD系统下。上面的轮询名词其实只是不同的轮询机制。不要被吓倒~~轮询技术虽然可以做到非阻塞,但其实是同步调用。有一种方式可以提供原生的AsynchronousI/O,但是适用范围较小,所以Node选择了另一种方式来实现完整的异步I/O。因此,所谓的Node单线程其实只是一个JavaScript主线程。那些耗时的异步操作,还是由线程池来完成。Node将这些耗时的操作丢给线程池处理,Node本身只需要往返调度即可。没有真正的I/O操作。单线程和CPU密集型单线程带来了不需要关心状态同步问题的好处,但也带来了几个弱点。它无法利用多核CPU。一个错误将导致整个应用程序退出CPU密集型任务。异步I/O将使Node.js失败。js中单线程解决CPU密集型任务的方法很粗糙,就是直接开启子进程,通过child_process将计算任务分发给子进程,然后进程间通过事件消息传递结果,即,进程间通信。(Node是用管道通信的~)什么,我对操作系统不熟悉,看来小伙伴们真的要好好补基础了~国庆期间不要出去玩了,哈哈哈~~活动-驱动事件驱动的本质就是通过主循环加事件触发来运行程序。事件循环的职责就是不断地等待事件的发生,然后按照订阅这个事件的时间顺序执行这个事件的所有处理器。当本次事件的所有handler都执行完毕后,事件循环会继续等待下一次事件的触发,如此循环往复。事件循环节点的事件循环使用libuv的默认事件循环,可以在src/node.cc。);启动事件循环boolmore;do{more=uv_run(env->event_loop(),UV_RUN_ONCE);如果(更多==false){EmitBeforeExit(env);//如果循环在发出事件后或在运行一些回调后变得活跃,则发出`beforeExit`。更多=uv_loop_alive(env->event_loop();(如果)uv_run(env->event_loop(),UV_RUN_NOWAIT)!=0)更多=true;}}while(more==true);code=EmitExit(env);RunAtExit(env);more用于标识是否进入下一轮循环。接下来,Node.js会根据more的情况来决定下一步。如果more为真,则继续运行下一轮循环。如果more为false,说明没有事件等待处理。EmitBeforeExit(env);触发进程的beforeExit事件,检查并处理相应的处理函数,完成后直接跳出循环。最后触发exit事件,执行相应的回调函数,Node.js操作结束,后面会进行一些资源释放操作。观察者每个事件循环中都会有观察者,判断是否有事件需要处理就是询问这些观察者。在Node.js中,事件的来源主要有网络请求、文件I/O等,这些事件对应不同的观察者。请求者怎么想的?不是那个物体,而是这个物体!!request对象是Node发起调用到内核执行I/O操作的过渡过程中产生的中间产物。例如,libuv调用文件I/O时,会立即返回FSReqWrap请求对象。JavaScript传入的参数和当前方法都封装在这个请求对象中,这个对象也会被推送到内核执行。事件驱动的优势事件循环、观察者、请求对象、I/O线程池共同构成了Node的事件驱动异步I/O模型。Apache采用为每个请求启动一个线程的方式来处理请求。线程虽然比较轻,但是还是需要占用一定的内存。当大并发请求到来时,内存占用会很高,导致服务器变慢。Node.js采用事件驱动的方式处理请求,无需为每个请求创建一个线程,可以节省大量的线程创建、销毁、系统上下文切换的开销,即使在大并发的情况下也能提供良好的性能。Nginx也采用了与Node相同的事件驱动模型。Nginx凭借其出色的性能,正逐渐取代Apache成为Web服务器的主流。综上所述,本文已收录GitHubhttps://github.com/ponkans/F2E(微怪整理的前端知识技能大树),欢迎Star,继续更新?怪怪上面提到的很多地方都不是很深入,但是大体框架结构类似,想了解更多的朋友可以参考《深入浅出Node.js》。Node生态日益壮大,前端开发小伙伴每天都会和它打交道。一些文章中提到的最基本的架构和概念,你应该已经掌握了。联系我/公众号微信搜索公众号【接水器】回复“加群”,我拉你进技术交流群。说实话,在这个群里,就算不说话,光看聊天记录也是一种成长。(阿里技术专家、敖丙作者、Java3y、蘑菇街高级前端、蚂蚁金服安全专家,各种大咖都有)。水怪也会定期创作原创作品,定期与朋友交流经验或帮忙看简历。注意,不要迷路,有机会一起跑吗?