node题目

189 阅读19分钟

node

特征:单线程、事件驱动、非阻塞I/O

模块

在 Node.js 模块系统中,每个文件都被视为一个独立的模块。 Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。 模块在第一次加载后会被缓存。 这也意味着(类似其他缓存机制)如果每次调用 require('foo') 都解析到同一文件,则返回相同的对象。 多次调用 require(foo) 不会导致模块的代码被执行多次。 模块是基于其解析的文件名进行缓存的。

CommonJS中require/exports和ES6中import/export区别

  • commonjs 模块它是“运行时”加载的;但 es6 import模块是“编译时”加载(引擎在编译代码时建立引用)。CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
  • CommonJS 输出是值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值;ES6 Modules输出的是值的引用,JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。被输出模块的内部的改变会影响引用的改变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

ES2020提案 引入import()函数,支持动态加载模块。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

module.exports,exports

  • 每次把要抛出的对象或方法赋值给exports的属性了 ,module.exports直接采用对象字面量的方式更加方便。
  • exports和module.exports都是引用类型的变量,而且这两个对象指向同一块内存地址。
  • 其次,exports对象是通过形参的方式传入的,直接赋值形参会改变形参的引用,但是并不能改变作用域外的值。 直接给exports赋值,会改变当前模块内部的形参exports对象的引用,也就是说当前的exports已经跟外部的module.exports对象没有任何关系了,所以这个改变是不会影响到module.exports的。因此,这种方式是没有任何效果的,所有的属性和方法都不会被抛出。 其实module.exports就是为了解决上述exports直接赋值,会导致抛出不成功的问题而产生的。

创建进程的方式

而Node有4种创建进程的方式:spawn(),exec(),execFile()和fork()

node 开启进程的方法有哪些,区别是什么

node中引入模块的机制

  1. 路径分析
  2. 文件定位
  3. 编译执行 优先从缓存中加载

AMD CMD规范的区别

  • CMD依赖就近,而AMD依赖前置
  • CMD是延迟执行的,而AMD是提前执行的
  • AMD的API默认是一个当多个用,CMD的API严格区分,推崇职责单一

node 底层

JS 代码跑在 V8 引擎上,Node.js 内置的fs、http等核心模块通过 C++ Bindings 调用 libuv、c-ares、llhttp 等 C/C++类库,从而接入操作系统提供的平台能力

在核心模块之下,有一层 C++ Bindings,将上层的 JavaScript 代码与下层 C/C++类库桥接起来

底层模块为了更好的性能,采用 C/C++实现,而上层的 JavaScript 代码无法直接与 C/C++通信,因而需要一个桥梁(即 Binding):另一方面,通过 Bindings 也可以复用可靠的老牌开源类库,而不必手搓所有底层模块

首先,编写的 JavaScript 代码由 V8 引擎来运行,运行中注册的事件监听会被保留下来,在对应的事件发生时收到通知

网络、文件 I/O 等事件产生时,已注册的回调函数将排到事件队列中,接着被事件循环取出放到调用栈上,回调函数执行完(调用栈清空)之后,事件循环再取一个放上去……

执行过程中遇到 I/O 操作就交给 libuv 线程池中的某个 woker 来处理,结束之后 libuv 产生一个事件放入事件队列。事件循环处理到返回事件时,对应的回调函数才在主线程开始执行,主线程在此期间继续其它工作,而不阻塞等待

Node.js 的运行原理,从左到右,从上到下,Node.js 被分为了四层,分别是 应用层、V8引擎层、Node API层 和 LIBUV层。

应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs
V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。
LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心 。

nodejs适合高并发

nodejs的I/O模型其实主要是由I/O多路复用和多线程下的阻塞I/O两种方式一起组成的,而应对高并发请求时发挥作用的主要就是I/O多路复用。好了,那最后我们来总结一下nodejs线程模型和I/O模型对比传统web应用多进(线)程 + 阻塞I/O模型的优势和劣势

nodejs利用单线程模型省去了系统维护和切换多进(线)程的开销,同时多路复用的I/O模型可以让nodejs的单线程不会阻塞在某一个连接上。在高并发场景下,nodejs应用只需要创建和管理多个客户端连接对应的socket描述符而不需要创建对应的进程或线程,系统开销上大大减少,所以能同时处理更多的客户端连接

nodejs并不能提升底层真正I/O操作的效率。如果底层I/O成为系统的性能瓶颈,nodejs依然无法解决,即nodejs可以接收高并发请求,但如果需要处理大量慢I/O操作(比如读写磁盘),仍可能造成系统资源过载。所以高并发并不能简单的通过单线程 + 非阻塞I/O模型来解决

CPU密集型应用可能会让nodejs的单线程模型成为性能瓶颈

nodejs适合高并发处理少量业务逻辑或快I/O(比如读写内存)

事件循环eventloop

事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去。

当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本(或丢入 REPL,本文不涉及到),它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick(),然后开始处理事件循环。

阶段概述

  • 定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测:setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。 在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。

每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。

每个循环阶段内容详解

  • timers阶段 一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。

注意:技术上来说,poll 阶段控制 timers 什么时候执行。

注意:这个下限时间有个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。

  • I/O callbacks阶段 这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED, 类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行. 名字会让人误解为执行I/O回调处理程序, 实际上I/O回调会由poll阶段处理.

  • poll阶段 poll 阶段有两个主要功能:(1)执行下限时间已经达到的timers的回调,(2)然后处理 poll 队列里的事件。 当event loop进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:

如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;

如果 poll 队列为空,则发生以下两件事之一:

如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里面的回调 callback)。 如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。

但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态): event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。

  • check阶段 这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。

setImmediate() 实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv的API 来设定在 poll 阶段结束后立即执行回调。

通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。

  • close callbacks 阶段 如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发

app.use和app.get区别

  • app.use(path,callback)中的callback既可以是router(路由)对象又可以是函数
  • app.get(path,callback)中的callback只能是函数

node 跨域

//解决跨域问题
        app.use(async(ctx, next) => {
            





            //指定服务器端允许进行跨域资源访问的来源域。可以用通配符*表示允许任何域的JavaScript访问资源,但是在响应一个携带身份信息(Credential)的HTTP请求时,必需指定具体的域,不能用通配符
            ctx.set("Access-Control-Allow-Origin", "*");

            //可选。它的值是一个布尔值,表示是否允许客户端跨域请求时携带身份信息(Cookie或者HTTP认证信息)。默认情况下,Cookie不包括在CORS请求之中。当设置成允许请求携带cookie时,需要保证"Access-Control-Allow-Origin"是服务器有的域名,而不能是"*";如果没有设置这个值,浏览器会忽略此次响应。
            ctx.set("Access-Control-Allow-Credentials", true);
            
            //指定服务器允许进行跨域资源访问的请求方法列表,一般用在响应预检请求上
            ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");
            
            //必需。指定服务器允许进行跨域资源访问的请求头列表,一般用在响应预检请求上
            ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type");
            // ctx.set("X-Powered-By", ' 3.2.1');
            
            //告诉客户端返回数据的MIME的类型,这只是一个标识信息,并不是真正的数据文件的一部分
            ctx.set("Content-Type", "application/json;charset=utf-8");
            
            //如果不设置mode,直接设置content-type为application/json,则fetch会默认这是跨域模式(mode:'cors'),在跨域POST之前,客户端会先发一条OPTIONS请求来”探探路”,如果服务器允许,再继续POST数据。对于这种OPTIONS请求,需要在服务器配置允许接受OPTIONS请求,这样写就是直接允许了所有的OPTIONS请求,也可以按照需求来判断OPTIONS请求中更详细的信息
            if (ctx.request.method == "OPTIONS") {
                ctx.response.status = 200
            }
            await next();
        });

什么是Stream?

stream是基于事件EventEmitter的数据管理模式.由各种不同的抽象接口组成,主要包括可写,可读,可读写,可转换等几种类型.

Stream有什么好处?

非阻塞式数据处理提升效率,片断处理节省内存,管道处理方便可扩展等.

内置的fs模块架构

  1. POSIX文件Wrapper,对应于操作系统的原生文件操作
  2. 文件流 fs.createReadStream和fs.createWriteStream
  3. 同步文件读写,fs.readFileSync和fs.writeFileSync
  4. 异步文件读写, fs.readFile和fs.writeFile

怎么读取json配置文件?

主要有两种方式,第一种是利用node内置的require('data.json')机制,直接得到js对象; 第二种是读入文件入内容,然后用JSON.parse(content)转换成js对象.

fs.watch和fs.watchFile有什么区别,怎么应用?

二者主要用来监听文件变动.fs.watch利用操作系统原生机制来监听,可能不适用网络文件系统; fs.watchFile则是定期检查文件状态变更,适用于网络文件系统,但是相比fs.watch有些慢,因为不是实时机制.

为什么需要child-process?

node是异步非阻塞的,这对高并发非常有效.可是我们还有其它一些常用需求,比如和操作系统shell命令交互,调用可执行文件,创建子进程进行阻塞式访问或高CPU计算等,child-process就是为满足这些需求而生的.child-process顾名思义,就是把node阻塞的工作交给子进程去做.

exec,execFile,spawn和fork都是做什么用的?

exec可以用操作系统原生的方式执行各种命令,如管道 cat ab.txt | grep hello; execFile是执行一个文件; spawn是流式和操作系统进行交互; fork是两个node程序(javascript)之间时行交互.

怎样让一个js文件变得像linux命令一样可执行?

  1. 在myCommand.js文件头部加入 #!/usr/bin/env node
  2. chmod命令把js文件改为可执行即可
  3. 进入文件目录,命令行输入myComand就是相当于node myComand.js了

child-process和process的stdin,stdout,stderror是一样的吗?

概念都是一样的,输入,输出,错误,都是流.区别是在父程序眼里,子程序的stdout是输入流,stdin是输出流.

node中的异步和同步怎么理解

node是单线程的,异步是通过一次次的循环事件队列来实现的.同步则是说阻塞式的IO,这在高并发环境会是一个很大的性能问题,所以同步一般只在基础框架的启动时使用,用来加载配置文件,初始化程序什么的.

有哪些方法可以进行异步流程的控制?

  1. 多层嵌套回调 2) 为每一个回调写单独的函数,函数里边再回调 3) 用第三方框架比方async, q, promise等

怎样绑定node程序到80端口?

多种方式 1) sudo 2) apache/nginx代理 3) 用操作系统的firewall iptables进行端口重定向

有哪些方法可以让node程序遇到错误后自动重启?

1)父进程监听子进程退出事件,参考 pm2、docker; 2)健康检查的模式,定时新启动一个进程查询目标进程的健康状态,退出了就重启。

怎样充分利用多个CPU?

一个CPU运行一个node实例

怎样调节node执行单元的内存大小?

用--max-old-space-size 和 --max-new-space-size 来设置 v8 使用内存的上限

程序总是崩溃,怎样找出问题在哪里?

  1. node --prof 查看哪些函数调用次数多
  2. memwatch和heapdump获得内存快照进行对比,查找内存溢出

有哪些常用方法可以防止程序崩溃?

  1. try-catch-finally
  2. EventEmitter/Stream error事件处理
  3. domain统一控制
  4. jshint静态检查
  5. jasmine/mocha进行单元测试

什么是错误优先的回调函数?

错误优先(Error-first)的回调函数(Error-First Callback)用于同时返回错误和数据。第一个参数返回错误,并且验证它是否出错;其他参数返回数据。

node导出模块有两种方式,一种是exports.xxx=xxx和Module.exports={}有什么区别吗

exports其实就是module.exports 如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值; 如果要输出一个函数或数组,必须直接对module.exports对象赋值。

内存泄漏

内存泄漏(Memory Leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。 如果内存泄漏的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用的内存变多会引起服务器响应速度变慢。 严重的情况下导致内存达到某个极限(可能是进程的上限,如 v8 的上限;也可能是系统可提供的内存上限)会使得应用程序崩溃。常见内存泄漏的原因内存泄漏的几种情况: 1) 全局变量 2) 闭包 3)事件监听

express:

在不断角逐中Express脱颖而出,Express–基于NodeJS平台,快速,开发,极简的web开发框架,这来自Express官方描述。Express包含application request response router 四大模块。Express在NodeJS的基础上完成二次抽象,封装处理细节向上提供丰富的模块方法以用来构建web应用,在此开发者只需通过这些功能方法开发中间件,扩展构建Web应用。 Express基于Es5语法,通过回调组合逻辑(类比如同jQuery中的ajax方法),回调的硬伤在于不可组合,难以捕获

Koa

一个更小、更富有表现力、更健壮的 Web 框架。 使用 Koa 编写 web 应用,通过组合不同的 Generator,可以免除重复繁琐的回调函数嵌套, 并极大地提升错误处理的效率。Koa 不在内核方法中绑定任何中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。这来自Koa 官方描述 。Koa 中也包含4个主要模块,Application、Request、Response、Context。Koa 定位为基础框架,相比Express更加小巧,几乎所有功能都通过中间件来完成,相应的Router模块从核心模块中去除,需通过中间件的形式集成,Koa 中三方router模块近20余种,开发者可以根据需求选择需要的模块集成或封装自己的模块用于构建应用,非常灵活,

Express和Koa2的区别和联系

一个简单的Express服务器

const express = require('express');
const app = express();

/* 中间件 */
app.use((req, res, next) => {
    console.log('middleware');
    next();
    console.log('middleware call');
});

/* 路由部分 */
const router = express.Router();
router.get('/', (req, res) => {
    res.send('Home');
});

app.use(router);

/* 静态文件 */
app.use(express.static('./'));

app.listen(3000);

一个简单的Koa服务器

const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');

const app = new Koa();
const router = Router();

/* 中间件 */
app.use(async (ctx, next) => {
    console.log('middleware');
    next();
    console.log('middleware call');
});

/* 路由部分 */
router.get('/', (ctx) => {
    ctx.body = 'Home';
});
app.use(router.routes());

/* 静态文件 */
app.use(serve('./'));

app.listen(3000);

用法的区别 Express是基于回调,也是node中最常见的Error-First的模式(第一个参数是error对象) Koa是使用的号称异步终极解决方案的Async/Await,也就是基于Promise,使用Try-Catch来捕获错误

中间件的区别 Express的中间件是线性模型 Koa的中间件是洋葱模型(聊一聊KOA的洋葱模型)

集成度 Express自带了Router和Static的中间件 Koa需要自行安装Router和Static的中间件

webSocket与传统的http有什么优势

  • 客户端与服务器只需要一个TCP连接,比http长轮询使用更少的连接
  • webSocket服务端可以推送数据到客户端
  • 更轻量的协议头,减少数据传输量