node简单点1| 8月更文挑战

206 阅读8分钟

阅读完您可以知道什么:

  • Node的特点
  • Node的应用场景
  • Node模块化和浏览器模块化的差异
  • Node为什么天生支持高并发

Node的特点

异步/IO

对于异步、在做前端开发、发送请求后我们并不是立马能过拿到数据、而是和服务器进行一系列操作(域名解析、三次握手、服务器通信/响应)当服务器给到我们数据会触发我们的异步回调方法、并走我们的方法逻辑、这个部分称为异步!在node中绝大多数的操作都是以异步的方式进行调用、这样做的好处是我们可以很自然的进行并行I/O操作、而无需等待前面的I/O结束、在编程模型上可以极大的提升效率。

事件与回调函数

我们在启动一个serve服务的时候、编写服务事件监听、并且把监听的结果用回调函数接收、事件编程具有轻量级、松耦合、只关注事物点等优势!但是也有别的问题就是事件与事件之间各自独立、如何协作是一个问题。

单线程

Node保持了Javascript在浏览器中单线程的特点,单线程最大的好处就是不用像多线程编程那样处处在意状态的同步问题、这里没有死锁的存在、也没有线程上下文交换所带来的性能开销!

  • 单线程的弱点(三方面)

    • 无法利用多核CPU
    • 程序错误会引起整个应用退出、应用的健壮性值得考验
    • 大量的计算占用CPU导致无法继续调用异步I/O

在Node中、长时间的CPU占用也会导致后续的异步I/O发不出调用、已完成的异步I/O的回调函数也不会得到及时的执行!Node为了解决这个问题采用了Web Workers相同的思路解决单线程大计算量的问题 child_process 、这样我们可以把大量的计算全部分解出去、然后通过进程间的事件消息来传递结果、我在编写Chrome插件也使用过这种方式去拆分计算逻辑降低运算造成的堵塞。

跨平台

起初Node只能在Linux平台运行、后面Node使用libuv实现跨平台操作!

graph TD
Node.js --> libuv
libuv --> *nix
libuv --> Windows

Node的应用场景

进行技术选型前、我们想要了解技术具体适合什么样的场景、这样我们才能做到在合适的场景使用合适的技术达到一个比较好的效果!关于Node探讨较多的是I/O密集型和CPU密集型;

I/O密集型

Node处理I/O的能力是值得称赞的、Node面向网络并且擅长并行I/O、能够有效的组织起更多的硬件资源!I/O密集的最大好处在于Node利用事件循环的处理能力、而不是为了每一个请求而去独立启动一个线程、资源占用极少!

CPU密集型

event loop在处理所有的任务/事件时,都是沿着事件队列顺序执行的,所以在其中任何一个任务/事件本身没有完成之前,其它的回调、监听器、超时、nextTick()的函数都得不到运行的机会,因为被阻塞的event loop根本没机会处理它们,此时程序最好的情况是变慢,最糟的情况是停滞不动,像死掉一样。所以当Node.js遇到高CPU占用率的任务时,event loop会被阻塞住,有解决方案吗?答案是有,Node虽然没有提供多线程用于计算支持、但是还是有以下两个方式来充分利用CPU。

  • Node可以通过编写C/C++扩展的方式更高效的利用CPU,将一些V8不能做到的性能极致的地方提供C/C++来实现
  • 如果单线程的Node不能满足需求、甚至用了C/C++扩展后还觉得不够、那么可以选择通过子进程的方式、将一部分Node进程当作常驻服务进程用于计算、利用进程间的消息来传递结果、将计算与I/O分离

Node模块机制

随着Javascript被广泛重视起来后、也暴露出一个问题,先天就缺乏的一项功能:模块、随着Node的诞生、后面社区也为其制定了相应的规范CommonJs规范 、当然在此之前也有很多的模块化技术、以往也有通过闭包或者一些命名空间还有AMD和RequireJS、今天我们只讲现在服务端Node所使用的模块CommonJs规范!

CommonJs模块规范

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见隔离开了、使得用户完全不必考虑变量被污染。通过require('xxx/地址') 引入模块 和通过 module.exports.xxx = xxx导出当前模块的方法或者变量

  • example
//mode.js
function add(a,b){
    return a + b;
}
module.exports.add = add; //导出模块方法

//index.js
const mode = require('./mode.js') //引入模块
console.log(mode) //{ add: [Function: add] 
let sum = mode.add(1,2);
console.log(sum) //3

在模块中,上下文提供了require()方法来引入外部模块。对应导出的模块来说,上下文提供了module对象、而exports是module的一个属性、并且它是唯一导出的入口、将想要导出的模块挂载到exports对象就可暴露给外部模块引入使用!

Node的模块实现

Node引入模块需要经历三个步骤 1:路径分析 2:文件定位 3:编译执行

Node中模块分为两类:一类是Node提供的模块,称为 核心模块,另外一种是用户编写的模块称之为文件模块 , 核心模块在Node源码编译中、就已经编译成二进制执行文件、在Node进程时、部分核心模块就已经被直接加载进内存中,文件模块 则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程、速度比核心模块慢。

浏览器会缓存静态脚本文件已提高性能、node对引入过的文件也会做缓存、以减少二次引入时的开销,不同的地方在于、浏览器仅仅缓存的文件、而Node缓存的事编译和执行之后的对象、不管是核心模块还是文件模块都具有缓存、不同的地方是核心模块缓存检查优先于文件模块的缓存检查。还有一种我称他为特殊模块也叫自定义模块、这种模块通常是第三方包、这类的模块查找是最费时间的、也是所有模块中最慢的一种!、他的查找过程有点类似原型链、在同层的node_moudles里找、找不到递归自父级、在找不到再往父级的父级、知道找到根节点 /

关于require文件扩展名分析:CommonJs允许你不写文件后缀、没有文件后缀的情况下Node会以 .js .json .node次序补齐扩展名、依次尝试,所以说如果是.json和.node文件我们自己给他补齐会快一些、还可以同步配合缓存、可以大幅度缓解Node单线程中阻塞式调用的缺陷。

AMD规范(CommonJs模块规范的延伸)

前后端的JavaScript分别搁置在http的两端、他们所扮演的角色各不相同、也正是因为如此前者依赖于带宽后者依赖CPU和内存资源、Node模块引入大部分都是同步的、如果前端也采用这种方式会造成体验感特别不好、鉴于网络原因CommonJs发现并不适合前端、所以出现一个AMD规范(异步模块定义) AMD文档地址

 define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
       exports.verb = function() {
           return beta.verb();
           //Or:
           return require("beta").verb();
       }
   });
   
   //创建一个名为"alpha"的模块,使用了require,exports,和名为"beta"的模块:
CMD规范

CMD规范是由国内玉伯提出、专门适用于浏览器、异步加载模块,只有模块被真正使用是才去执行加载、CMD规范整合了CommonJS和AMD规范的特点

define(function(require, exports, module){
  var module2 = require('./module')
  //也可以引入依赖模块(异步)
  require.async('./module1', function (m3) {
      ...
  })
  //暴露模块
  exports.xxx = value
})
ES6模块化

ES6模块化,也是现在用的最多的模块化技术,在ES6中每一个模块即是一个文件,在文件中定义的变量,函数,对象在外部是无法获取的。您需要把它暴露出去才可以被使用!

  • import和require的区别

    • 导入的方式
 //【es6】
 //index.js
 import mode from "./mode.js"
 console.log(mode(1,2))
 //mode.js
 function add(a,b){
    return a + b
}
export default add;

//或者
//mode.js
export function add(a,b){ 
 return a + b
}
//index
import { add } from "./mode.js" 

总结:ES6模块和CommonJs模块主要有以下两大区别

1、CommonJs模块输出的是一个值的拷贝,ES6模块输出的是值的引用。

2、CommonJs模块是运行时加载,ES6模块是编译时输出接口。