1.CommonJS 和 ES6 Module 究竟有什么区别?
CommonJS
CommonJS 的模块主要由原生模块 module 来实现,这个类上的一些属性对我们理解模块机制有很大帮助
Module {
id: '.', // 如果是 mainModule id 固定为 '.',如果不是则为模块绝对路径
exports: {}, // 模块最终 exports
filename: '/absolute/path/to/entry.js', // 当前模块的绝对路径
loaded: false, // 模块是否已加载完毕
children: [], // 被该模块引用的模块
parent: '', // 第一个引用该模块的模块
paths: [ // 模块的搜索路径
'/absolute/path/to/node_modules',
'/absolute/path/node_modules',
'/absolute/node_modules',
'/node_modules'
]
}
require 从哪里来?
在编写 CommonJS 模块的时候,我们会使用 require 来加载模块,使用 exports 来做模块输出,还有 module,__filename, __dirname 这些变量,为什么它们不需要引入就能使用?
原因是 Node 在解析 JS 模块时,会先按文本读取内容,然后将模块内容进行包裹,在外层裹了一个 function,传入变量
再通过 vm.runInThisContext 将字符串转成 Function形成作用域,避免全局污染。
let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
于是在 CommmonJS 的模块中可以不需要 require,直接访问到这些方法,变量。
参数中的 module 是当前模块的的 module 实例(尽管这个时候模块代码还没编译执行),
exports 是 module.exports 的别名,最终被 require 的时候是输出 module.exports 的值,
require 最终调用的也是 Module._load 方法。
__filename,__dirname 则分别是当前模块在系统中的绝对路径和当前文件夹路径。
ES6 模块
ES6 模块是前端开发同学更为熟悉的方式,使用 import, export 关键字来进行模块输入输出。ES6 不再是使用闭包和函数封装的方式进行模块化,而是从语法层面提供了模块化的功能。
ES6 模块中不存在 require, module.exports, __filename 等变量,CommonJS 中也不能使用 import。两种规范是不兼容的,一般来说平日里写的 ES6 模块代码最终都会经由 Babel, Typescript 等工具处理成 CommonJS 代码。
使用 Node 原生 ES6 模块需要将 js 文件后缀改成 mjs,或者 package.json "type"`` 字段改为 "module",通过这种形式告知Node使用ES Module` 的形式加载模块。
为什么平时开发可以混写?
前面提到 ES6 模块和 CommonJS 模块有很大差异,不能直接混着写。
这和开发中表现是不一样的,原因是开发中写的 ES6 模块最终都会被打包工具处理成 CommonJS 模块,以便兼容更多环境,
同时也能和当前社区普通的 CommonJS 模块融合。
2.关于webpack,babel,以及es6和commonJS之间的联系
现在的浏览器很多都不支持es6的语法,或者仅仅是部分支持,比如你用.360浏览器,你会发现它支持let却不支持箭头函数等。
babel就承担了“翻译”的角色,把es6的写法转换成es5的写法。
babel转换后的代码是遵循commonJS规范的,而这个规范,浏览器并不能识别。因此导入到浏览器中会报错,而nodeJS是commonJS的实现者,所以在babel转换后的代码是可以在node中运行的
为了将babel生成的commonJS规范的es5写法能够在浏览器上直接运行,我们就借住了webpack这个打包工具来完成,
因为webpack本身也是遵循commonJS这个规范的,从它的配置文件webpack.config.js中就可以看出来
//module.exports是commonJS的接口输出规范,es6的规范是export
module.exports = {
entry: path.join(__dirname, 'index.js'),
output: {
path: path.join(__dirname, 'outs'),
filename: 'index.js'
},
};
babel:把es6的写法转换成es5的写法
webpack:将babel生成的commonJS规范的es5写法能够在浏览器上直接运行
3.path.resolve和path.join的区别?
两者都是拼接文件路径
path.resolve 获取绝对路径
path.join 获取在相对路径
4.Node.js的Event Loop
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。
process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2
上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。
那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2。
令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。
5.session如何实现登录
session 存储到redis
6.请描述koa2和express的中间件机制
koa2中间件实例
koa2的中间件是通过 async await 实现的,中间件执行顺序是“洋葱圈”模型。
中间件之间通过next函数联系,当一个中间件调用 next() 后,会将控制权交给下一个中间件, 直到下一个中间件不再执行 next() 后, 将会沿路折返,将控制权依次交换给前一个中间件
app.js
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
console.log('第一层 - 开始')
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ----------- ${ctx.url} ----------- ${rt}`);
console.log('第一层 - 结束')
});
// x-response-time
app.use(async (ctx, next) => {
console.log('第二层 - 开始')
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
console.log('第二层 - 结束')
});
// response
app.use(async ctx => {
console.log('第三层 - 开始')
ctx.body = 'Hello World';
console.log('第三层 - 结束')
});
app.listen(3000);
控制台输出:
第一层 - 开始
第二层 - 开始
第三层 - 开始
第三层 - 结束
第二层 - 结束
打印第一次执行的结果: GET -------- /text ------ 4ms
第一层 - 结束
express中间件
与 koa2 中间件不同的是,express中间件一个接一个的顺序执行, 通常会将 response 响应写在最后一个中间件中
主要特点:
app.use 用来注册中间件
遇到 http 请求,根据 path 和 method 判断触发哪些中间件
实现 next 机制,即上一个中间件会通过 next 触发下一个中间件
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('第一层 - 开始')
setTimeout(() => {
next()
}, 0)
console.log('第一层 - 结束')
})
app.use((req, res, next) => {
console.log('第二层 - 开始')
setTimeout(() => {
next()
}, 0)
console.log('第二层 - 结束')
})
app.use('/api', (req, res, next) => {
console.log('第三层 - 开始')
res.json({
code: 0
})
console.log('第三层 - 结束')
})
app.listen(3000, () => {
console.log('server is running on port 3000')
})
控制台输出:
第一层 - 开始
第一层 - 结束
第二层 - 开始
第二层 - 结束
第三层 - 开始
第三层 - 结束
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('第一层 - 开始')
next()
console.log('第一层 - 结束')
})
app.use((req, res, next) => {
console.log('第二层 - 开始')
next()
console.log('第二层 - 结束')
})
app.use('/api', (req, res, next) => {
console.log('第三层 - 开始')
res.json({
code: 0
})
console.log('第三层 - 结束')
})
app.listen(3000, () => {
console.log('server is running on port 3000')
})
控制台输出:
第一层 - 开始
第二层 - 开始
第三层 - 开始
第三层 - 结束
第二层 - 结束
第一层 - 结束
7.如何读取一个1g大小的日志文件
8.nodejs是单线程还是多线程
Node.js的单线程指的是主线程是“单线程”,由主要线程去按照编码顺序一步步执行程序代码,假如遇到同步代码阻塞,主线程被占用,后续的程序代码执行就会被卡住。
nodejs运行机制:
a、V8引擎解析JavaScript脚本
b、解析后的代码,调用Node API
c、libuv(c++库)库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
d、V8引擎再将结果返回给用户
单线程怎么实现高并发
每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。
主线程之外,还维护了一个"事件队列"(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
3、单线程的好处:
(1)多线程占用内存高
(2)多线程间切换使得CPU开销大
(3)多线程由内存同步开销
(4)编写单线程程序简单
(5)线程安全
4、单线程的劣势:
(1)CPU密集型任务占用CPU时间长(可通过cluster方式解决)
(2)无法利用CPU的多核(可通过cluster方式解决)
(3)单线程抛出异常使得程序停止(可通过try catch方式或自动重启机制解决)
node可以实现多进程
1\. 创建多进程的模块
1.1 child_process
1.2 cluster
2. 创建多进程的方法
2.1 child_process有4种方法:
1. spawn: 创建子进程,执行非node程序,执行结果以流形式返回
2. execFile: 创建子进程,执行非node程序,执行结果以回调返回
3. exec: 创建子进程,执行shell命令,执行结果以回调返回,可以直接执行一串shell命令
4. fork: 创建子进程,执行node程序,执行结果以流返回
2.2 cluster有1种方法:
cluster.fork:创建一个子进程
3. nodejs多进程模型
nodejs的多进程是master-work模式,一个主进程中,创建出多个子进程
4\ 进程间消息传递
通过on('message')监听和send分发
5. 多个work监听同一个端口
nodejs中的多个work进程可以同时监听同一个端口。具体实现方式,child_process和cluster有区别。
5.1 child_process方式
child_process通常在master进程中创建socket或server,
通过send方法将socket或server,发送到worker进程,在worker进程中同时监听一个端口
5.2 cluster方式
cluster通过cluster.isMaster和cluster.isWorker来区分主进程和worker进程,在worker进程中,
可以直接启动server,来监听同一个端口cluster主要是提供了对多个子进程的管理,
子进程的创建、退出和重启,可以方便的进行。