Tiny'Wo | 小窝

网络中的一小块自留地

面试官:说说 Node. js 有哪些全局对象�?

一、是什�?

在浏览器 JavaScript 中,通常 window 是全局对象�?�?Nodejs 中的全局对象�?global

NodeJS里,是不可能在最外层定义一个变量,因为所有的用户代码都是当前模块的,只在当前模块里可用,但可以通过exports对象的使用将其传递给模块外部

所以,在NodeJS中,用var声明的变量并不属于全局的变量,只在当前模块生效

像上述的global全局对象则在全局作用域中,任何全局变量、函数、对象都是该对象的一个属性�?

二、有哪些

将全局对象分成两类�?

  • 真正的全局对象

  • 模块级别的全局变量

真正的全局对象

下面给出一些常见的全局对象�?

  • Class:Buffer

  • process

  • console

  • clearInterval、setInterval

  • clearTimeout、setTimeout

  • global

Class:Buffer

可以处理二进制以及非Unicode编码的数�?
Buffer类实例化中存储了原始数据。Buffer类似于一个整数数组,在V8堆原始存储空间给它分配了内存

一旦创建了Buffer实例,则无法改变大小

process

进程对象,提供有关当前进程的信息和控�?
包括在执行node程序进程时,如果需要传递参数,我们想要获取这个参数需要在process内置对象�?
启动进程�?

1
node index.js 参数1 参数2 参数3

index.js文件如下�?

1
2
3
process.argv.forEach((val, index) => {
console.log(`${index}: ${val}`);
});

输出如下�?

1
2
3
4
5
/usr/local/bin/node
/Users/mjr/work/node/process-args.js
参数1
参数2
参数3

除此之外,还包括一些其他信息如版本、操作系统等

console

用来打印stdoutstderr

最常用的输入内容的方式:console.log

1
console.log("hello");

清空控制台:console.clear

1
console.clear

打印函数的调用栈:console.trace

1
2
3
4
5
6
7
8
9
10
11
12
13
function test() {
demo();
}

function demo() {
foo();
}

function foo() {
console.trace();
}

test();

clearInterval、setInterval

设置定时器与清除定时�?

1
setInterval(callback, delay[, ...args])

callbackdelay毫秒重复执行一�?
clearInterval则为对应发取消定时器的方�?

clearTimeout、setTimeout

设置延时器与清除延时�?

1
setTimeout(callback,delay[,...args])

callbackdelay毫秒后执行一�?
clearTimeout则为对应取消延时器的方法

global

全局命名空间对象,墙面讲到的processconsolesetTimeout等都有放到global�?

1
console.log(process === global.process) // true

模块级别的全局对象

这些全局对象是模块中的变量,只是每个模块都有,看起来就像全局变量,像在命令交互中是不可以使用,包括:

  • __dirname
  • __filename
  • exports
  • module
  • require

__dirname

获取当前文件所在的路径,不包括后面的文件名

�?/Users/mjr 运行 node example.js�?

1
2
console.log(__dirname);
// 打印: /Users/mjr

__filename

获取当前文件所在的路径和文件名称,包括后面的文件名�?
�?/Users/mjr 运行 node example.js�?

1
2
console.log(__filename);
// 打印: /Users/mjr/example.js

exports

module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内�?

1
2
3
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;

module

对当前模块的引用,通过module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内�?

require

用于引入模块�?JSON、或本地文件�?可以�?node_modules 引入模块�?
可以使用相对路径引入本地模块或JSON文件,路径会根据__dirname定义的目录名或当前工作目录进行处�?

参考文�?

面试官:如何实现jwt鉴权机制?说说你的思路

一、是什�?

JWT(JSON Web Token),本质就是一个字符串书写规范,如下图,作用是用来在用户和服务器之间传递安全可靠的信息

在目前前后端分离的开发过程中,使用token鉴权机制用于身份验证是最常见的方案,流程如下�?

  • 服务器当验证用户账号和密码正确的时候,给用户颁发一个令牌,这个令牌作为后续用户访问一些接口的凭证
  • 后续访问会根据这个令牌判断用户时候有权限进行访问

Token,分成了三部分,头部(Header)、载荷(Payload)、签名(Signature),并以.进行拼接。其中头部和载荷都是以JSON格式存放数据,只是进行了编码

每个JWT都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为alg,同时还有一个typ的字段,默认JWT即可。以下示例中算法为HS256

1
{  "alg": "HS256",  "typ": "JWT" } 

因为JWT是字符串,所以我们还需要对以上内容进行Base64编码,编码后字符串如下:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9        

payload

载荷即消息体,这里会存放实际的内容,也就是Token的数据声明,例如用户的idname,默认情况下也会携带令牌的签发时间iat,通过还可以设置过期时间,如下�?

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

同样进行Base64编码后,字符串如下:

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

Signature

签名是对头部和载荷内容进行签名,一般情况,设置一个secretKey,对前两个的结果进行HMACSHA25算法,公式如下:

1
Signature = HMACSHA256(base64Url(header)+.+base64Url(payload),secretKey)

一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一�?

二、如何实�?

Token的使用分成了两部分:

  • 生成token:登录成功的时候,颁发token
  • 验证token:访问某些资源或者接口时,验证token

生成 token

借助第三方库jsonwebtoken,通过jsonwebtoken �?sign 方法生成一�?token�?

  • 第一个参数指的是 Payload

  • 第二个是秘钥,服务端特有

  • 第三个参数是 option,可以定�?token 过期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const crypto = require("crypto"),
jwt = require("jsonwebtoken");
// TODO:使用数据�?// 这里应该是用数据库存储,这里只是演示�?let userList = [];

class UserController {
// 用户登录
static async login(ctx) {
const data = ctx.request.body;
if (!data.name || !data.password) {
return ctx.body = {
code: "000002",
message: "参数不合�?
}
}
const result = userList.find(item => item.name === data.name && item.password === crypto.createHash('md5').update(data.password).digest('hex'))
if (result) {
// 生成token
const token = jwt.sign(
{
name: result.name
},
"test_token", // secret
{ expiresIn: 60 * 60 } // 过期时间�?0 * 60 s
);
return ctx.body = {
code: "0",
message: "登录成功",
data: {
token
}
};
} else {
return ctx.body = {
code: "000002",
message: "用户名或密码错误"
};
}
}
}

module.exports = UserController;

在前端接收到token后,一般情况会通过localStorage进行缓存,然后将token放到HTTP 请求头Authorization 中,关于Authorization 的设置,前面要加�?Bearer ,注意后面带有空�?

1
2
3
4
5
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
config.headers.common['Authorization'] = 'Bearer ' + token; // 留意这里�?Authorization
return config;
})

校验token

使用 koa-jwt 中间件进行验证,方式比较简�?

1
2
3
4
/ 注意:放在路由前�?app.use(koajwt({
secret: 'test_token'
}).unless({ // 配置白名�? path: [/\/api\/register/, /\/api\/login/]
}))
  • secret 必须�?sign 时候保持一�?- 可以通过 unless 配置接口白名单,也就是哪�?URL 可以不用经过校验,像登陆/注册都可以不用校�?- 校验的中间件需要放在需要校验的路由前面,无法对前面�?URL 进行校验

获取token用户的信息方法如下:

1
2
3
4
5
router.get('/api/userInfo',async (ctx,next) =>{
const authorization = ctx.header.authorization // 获取jwt
const token = authorization.replace('Beraer ','')
const result = jwt.verify(token,'test_token')
ctx.body = result

注意:上述的HMA256加密算法为单秘钥的形式,一旦泄露后果非常的危险

在分布式系统中,每个子系统都要获取到秘钥,那么这个子系统根据该秘钥可以发布和验证令牌,但有些服务器只需要验证令�?
这时候可以采用非对称加密,利用私钥发布令牌,公钥验证令牌,加密算法可以选择RS256

三、优缺点

优点�?

  • json具有通用性,所以可以跨语言
  • 组成简单,字节占用小,便于传输
  • 服务端无需保存会话信息,很容易进行水平扩展
  • 一处生成,多处使用,可以在分布式系统中,解决单点登录问�?- 可防护CSRF攻击

缺点�?

  • payload部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息
  • 需要保护好加密密钥,一旦泄露后果不堪设�?- 为避免token被劫持,最好使用https协议

参考文�?

面试官:说说对中间件概念的理解,如何封装 node 中间件?

一、是什�?

中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的

NodeJS中,中间件主要是指封装http请求细节处理的方�?
例如在expresskoaweb框架中,中间件的本质为一个回调函数,参数包含请求对象、响应对象和执行下一个中间件的函�?

在这些中间件函数中,我们可以执行业务逻辑代码,修改请求和响应对象、返回响应数据等操作

二、封�?

koa是基于NodeJS当前比较流行的web框架,本身支持的功能并不多,功能都可以通过中间件拓展实现。通过添加不同的中间件,实现不同的需求,从而构建一�?Koa 应用

Koa 中间件采用的是洋葱圈模型,每次执行下一个中间件传入两个参数�?

  • ctx :封装了request �? response 的变�?- next :进入下一个要执行的中间件的函�?

下面就针对koa进行中间件的封装�?
Koa 的中间件就是函数,可以是 async 函数,或是普通函�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// async 函数
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

// 普通函�?app.use((ctx, next) => {
const start = Date.now();
return next().then(() => {
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
});

下面则通过中间件封装http请求过程中几个常用的功能�?

token校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = (options) => async (ctx, next) {
try {
// 获取 token
const token = ctx.header.authorization
if (token) {
try {
// verify 函数验证 token,并获取用户相关信息
await verify(token)
} catch (err) {
console.log(err)
}
}
// 进入下一个中间件
await next()
} catch (err) {
console.log(err)
}
}

日志模块

1
2
3
4
5
6
7
8
9
10
const fs = require('fs')
module.exports = (options) => async (ctx, next) => {
const startTime = Date.now()
const requestTime = new Date()
await next()
const ms = Date.now() - startTime;
let logout = `${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms`;
// 输出日志文件
fs.appendFileSync('./log.txt', logout + '\n')
}

Koa存在很多第三方的中间件,如koa-bodyparserkoa-static�?
下面再来看看它们的大体的简单实现:

koa-bodyparser

koa-bodyparser 中间件是将我们的 post 请求和表单提交的查询字符串转换成对象,并挂在 ctx.request.body 上,方便我们在其他中间件或接口处取�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 文件:my-koa-bodyparser.js
const querystring = require("querystring");

module.exports = function bodyParser() {
return async (ctx, next) => {
await new Promise((resolve, reject) => {
// 存储数据的数�? let dataArr = [];

// 接收数据
ctx.req.on("data", data => dataArr.push(data));

// 整合数据并使�?Promise 成功
ctx.req.on("end", () => {
// 获取请求数据的类�?json 或表�? let contentType = ctx.get("Content-Type");

// 获取数据 Buffer 格式
let data = Buffer.concat(dataArr).toString();

if (contentType === "application/x-www-form-urlencoded") {
// 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body
ctx.request.body = querystring.parse(data);
} else if (contentType === "applaction/json") {
// 如果�?json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
ctx.request.body = JSON.parse(data);
}

// 执行成功的回�? resolve();
});
});

// 继续向下执行
await next();
};
};

koa-static

koa-static 中间件的作用是在服务器接到请求时,帮我们处理静态文�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const fs = require("fs");
const path = require("path");
const mime = require("mime");
const { promisify } = require("util");

// �?stat �?access 转换�?Promise
const stat = promisify(fs.stat);
const access = promisify(fs.access)

module.exports = function (dir) {
return async (ctx, next) => {
// 将访问的路由处理成绝对路径,这里要使�?join 因为有可能是 /
let realPath = path.join(dir, ctx.path);

try {
// 获取 stat 对象
let statObj = await stat(realPath);

// 如果是文件,则设置文件类型并直接响应内容,否则当作文件夹寻找 index.html
if (statObj.isFile()) {
ctx.set("Content-Type", `${mime.getType()};charset=utf8`);
ctx.body = fs.createReadStream(realPath);
} else {
let filename = path.join(realPath, "index.html");

// 如果不存在该文件则执�?catch 中的 next 交给其他中间件处�? await access(filename);

// 存在设置文件类型并响应内�? ctx.set("Content-Type", "text/html;charset=utf8");
ctx.body = fs.createReadStream(filename);
}
} catch (e) {
await next();
}
}
}

三、总结

在实现中间件时候,单个中间件应该足够简单,职责单一,中间件的代码编写应该高效,必要的时候通过缓存重复获取数据

koa本身比较简洁,但是通过中间件的机制能够实现各种所需要的功能,使得web应用具备良好的可拓展性和组合�?
通过将公共逻辑的处理编写在中间件中,可以不用在每一个接口回调中做相同的代码编写,减少了冗杂代码,过程就如装饰者模�?

参考文�?

面试官:说说你对Node.js 的理解?优缺点?应用场景�?

一、是什�?Node.js 是一个开源与跨平台的 JavaScript 运行时环�?

在浏览器外运�?V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能

可以理解�?Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript运行环境

非阻塞异�?

Nodejs采用了非阻塞型I/O机制,在做I/O操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作

例如在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效�?

事件驱动

事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函�?
比如读取一个文件,文件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进行处理

二、优缺点

优点�?

  • 处理高并发场景性能更佳
  • 适合I/O密集型应用,值的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在�?I/O硬盘内存读写操作

因为Nodejs是单线程,带来的缺点有:

  • 不适合CPU密集型应�?- 只支持单核CPU,不能充分利用CPU
  • 可靠性低,一旦代码某个环节崩溃,整个系统都崩�?

三、应用场�?

借助Nodejs的特点和弊端,其应用场景分类如下�?

  • 善于I/O,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程

  • 大量并发的I/O,应用程序内部并不需要进行非常复杂的处理

  • �?websocket 配合,开发长连接的实时交互应用程�?
    具体场景可以表现为如下:

  • 第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序

  • 第二大类:基于web、canvas等多人联网游�?- 第三大类:基于web的多人实时聊天客户端、聊天室、图文直�?- 第四大类:单页面浏览器应用程�?- 第五大类:操作数据库、为前端和移动端提供基于json的API

其实,Nodejs能实现几乎一切的应用,只考虑适不适合使用�?

参考文�?

面试官:如果让你来设计一个分页功�? 你会怎么设计? 前后端如何交�?

一、是什�?

在我们做数据查询的时候,如果数据量很大,比如几万条数据,放在一个页面显示的话显然不友好,这时候就需要采用分页显示的形式,如每次只显�?0条数�?

要实现分页功能,实际上就是从结果集中显示�?10条记录作为第1页,显示�?120条记录作为第2页,以此类推

因此,分页实际上就是从结果集中截取出第M~N条记�?

二、如何实�?

前端实现分页功能,需要后端返回必要的数据,如总的页数,总的数据量,当前页,当前的数�?

1
2
3
4
5
6
7
8
{
"totalCount": 1836, // 总的条数
"totalPages": 92, // 总页�? "currentPage": 1 // 当前页数
"data": [ // 当前页的数据
{
...
}
]

后端采用mysql作为数据的持久性存�?
前端向后端发送目标的页码page以及每页显示数据的数量pageSize,默认情况每次取10条数据,则每一条数据的起始位置start为:

1
const start = (page - 1) * pageSize

当确定了limitstart的值后,就能够确定SQL语句�?

1
const sql = `SELECT * FROM record limit ${pageSize} OFFSET ${start};`

上诉SQL语句表达的意思为:截取从startstart+pageSize之间(左闭右开)的数据

关于查询数据总数的SQL语句为,record为表名:

1
SELECT COUNT(*) FROM record

因此后端的处理逻辑为:

  • 获取用户参数页码数page和每页显示的数目 pageSize ,其中page 是必须传递的参数,pageSize为可选参数,默认�?0
  • 编写 SQL 语句,利�?limit �?OFFSET 关键字进行分页查�?- 查询数据库,返回总数据量、总页数、当前页、当前页数据给前�?
    代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
router.all('/api', function (req, res, next) {
var param = '';
// 获取参数
if (req.method == "POST") {
param = req.body;
} else {
param = req.query || req.params;
}
if (param.page == '' || param.page == null || param.page == undefined) {
res.end(JSON.stringify({ msg: '请传入参数page', status: '102' }));
return;
}
const pageSize = param.pageSize || 10;
const start = (param.page - 1) * pageSize;
const sql = `SELECT * FROM record limit ${pageSize} OFFSET ${start};`
pool.getConnection(function (err, connection) {
if (err) throw err;
connection.query(sql, function (err, results) {
connection.release();
if (err) {
throw err
} else {
// 计算总页�? var allCount = results[0][0]['COUNT(*)'];
var allPage = parseInt(allCount) / 20;
var pageStr = allPage.toString();
// 不能被整�? if (pageStr.indexOf('.') > 0) {
allPage = parseInt(pageStr.split('.')[0]) + 1;
}
var list = results[1];
res.end(JSON.stringify({ msg: '操作成功', status: '200', totalPages: allPage, currentPage: param.page, totalCount: allCount, data: list }));
}
})
})
});

三、总结

通过上面的分析,可以看到分页查询的关键在于,要首先确定每页显示的数量pageSize,然后根据当前页的索引pageIndex(从1开始),确定LIMITOFFSET应该设定的值:

  • LIMIT 总是设定�?pageSize
  • OFFSET 计算公式�?pageSize * (pageIndex - 1)

确定了这两个值,就能查询出第 N页的数据

参考文�?

面试官:Node性能如何进行监控以及优化�?

一�?是什�?

Node作为一门服务端语言,性能方面尤为重要,其衡量指标一般有如下�?

  • CPU
  • 内存
  • I/O
  • 网络

CPU

主要分成了两部分�?

  • CPU负载:在某个时间段内,占用以及等待CPU的进程总数
  • CPU使用率:CPU时间占用状况,等�?1 - 空闲CPU时间(idle time) / CPU总时�?
    这两个指标都是用来评估系统当前CPU的繁忙程度的量化指标

Node应用一般不会消耗很多的CPU,如果CPU占用率高,则表明应用存在很多同步操作,导致异步任务回调被阻塞

内存指标

内存是一个非常容易量化的指标�?内存占用率是评判一个系统的内存瓶颈的常见指标�?对于Node来说,内部内存堆栈的使用状态也是一个可以量化的指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /app/lib/memory.js
const os = require('os');
// 获取当前Node内存堆栈情况
const { rss, heapUsed, heapTotal } = process.memoryUsage();
// 获取系统空闲内存
const sysFree = os.freemem();
// 获取系统总内�?const sysTotal = os.totalmem();

module.exports = {
memory: () => {
return {
sys: 1 - sysFree / sysTotal, // 系统内存占用�? heap: heapUsed / headTotal, // Node堆内存占用率
node: rss / sysTotal, // Node占用系统内存的比�? }
}
}
  • rss:表示node进程占用的内存总量�?- heapTotal:表示堆内存的总量�?- heapUsed:实际堆内存的使用量�?- external :外部程序的内存使用量,包含Node核心的C++程序的内存使用量

Node中,一个进程的最大内存容量为1.5GB。因此我们需要减少内存泄�?

磁盘 I/O

硬盘的 IO 开销是非常昂贵的,硬�?IO 花费�?CPU 时钟周期是内存的 164000 �?
内存 IO 比磁盘 IO 快非常多,所以使用内存缓存数据是有效的优化方法。常用的工具�?redismemcached �?
并不是所有数据都需要缓存,访问频率高,生成代价比较高的才考虑是否缓存,也就是说影响你性能瓶颈的考虑去缓存,并且而且缓存还有缓存雪崩、缓存穿透等问题要解�?

二、如何监�?

关于性能方面的监控,一般情况都需要借助工具来实�?
这里采用Easy-Monitor 2.0,其是轻量级�?Node.js 项目内核性能监控 + 分析工具,在默认模式下,只需要在项目入口文件 require 一次,无需改动任何业务代码即可开启内核级别的性能监控分析

使用方法如下�?
在你的项目入口文件中按照如下方式引入,当然请传入你的项目名称�?

1
2
const easyMonitor = require('easy-monitor');
easyMonitor('你的项目名称');

打开你的浏览器,访问 http://localhost:12333 ,即可看到进程界�?
关于定制化开发、通用配置项以及如何动态更新配置项详见官方文档

三、如何优�?

关于Node的性能优化的方式有�?

  • 使用最新版本Node.js
  • 正确使用�?Stream
  • 代码层面优化
  • 内存管理优化

使用最新版本Node.js

每个版本的性能提升主要来自于两个方面:

  • V8 的版本更�?- Node.js 内部代码的更新优�?

正确使用�?Stream

Node中,很多对象都实现了流,对于一个大文件可以通过流的形式发送,不需要将其完全读入内�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const http = require('http');
const fs = require('fs');

// bad
http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, data) {
res.end(data);
});
});

// good
http.createServer(function (req, res) {
const stream = fs.createReadStream(__dirname + '/data.txt');
stream.pipe(res);
});

代码层面优化

合并查询,将多次查询合并一次,减少数据库的查询次数

1
2
3
4
5
6
7
8
9
10
// bad
for user_id in userIds
let account = user_account.findOne(user_id)

// good
const user_account_map = {} // 注意这个对象将会消耗大量内存�?user_account.find(user_id in user_ids).forEach(account){
user_account_map[account.user_id] = account
}
for user_id in userIds
var account = user_account_map[user_id]

内存管理优化

�?V8 中,主要将内存分为新生代和老生代两代:

  • 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象
  • 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对�?
    若新生代内存空间不够,直接分配到老生�?
    通过减少内存占用,可以提高服务器的性能。如果有内存泄露,也会导致大量的对象存储到老生代中,服务器性能会大大降�?
    如下面情况:
1
2
3
4
5
6
7
8
9
10
11
12
const buffer = fs.readFileSync(__dirname + '/source/index.htm');

app.use(
mount('/', async (ctx) => {
ctx.status = 200;
ctx.type = 'html';
ctx.body = buffer;
leak.push(fs.readFileSync(__dirname + '/source/index.htm'));
})
);

const leak = [];

leak的内存非常大,造成内存泄露,应当避免这样的操作,通过减少内存使用,是提高服务性能的手段之一

而节省内存最好的方式是使用池,其将频用、可复用对象存储起来,减少创建和销毁操�?
例如有个图片请求接口,每次请求,都需要用到类。若每次都需要重新new这些类,并不是很合适,在大量请求时,频繁创建和销毁这些类,造成内存抖动

使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中。每次用到该对象时,就取对象池空闲的对象,并对它进行初始化操作,从而提高框架的性能

参考文�?

面试官:说说�?Node 中的 process 的理解?有哪些常用方法?

一、是什�?

process 对象是一个全局变量,提供了有关当前 Node.js 进程的信息并对其进行控制,作为一个全局变量

我们都知道,进程计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是线程的容�?
当我们启动一个js文件,实际就是开启了一个服务进程,每个进程都拥有自己的独立空间地址、数据栈,像另一个进程无法访问当前进程的变量、数据结构,只有数据通信后,进程之间才可以数据共�?
由于JavaScript是一个单线程语言,所以通过node xxx启动一个文件后,只有一条主线程

二、属性与方法

关于process常见的属性有如下�?

  • process.env:环境变量,例如通过 `process.env.NODE_ENV 获取不同环境项目配置信息
  • process.nextTick:这个在谈及 EventLoop 时经常为会提�?- process.pid:获取当前进程id
  • process.ppid:当前进程对应的父进�?- process.cwd():获取当前进程工作目录,
  • process.platform:获取当前进程运行的操作系统平台
  • process.uptime():当前进程已运行时间,例如:pm2 守护进程�?uptime �?- 进程事件�?process.on(‘uncaughtException�?cb) 捕获异常信息�?process.on(‘exit�?cb)进程推出监�?- 三个标准流: process.stdout 标准输出�?process.stdin 标准输入�?process.stderr 标准错误输出
  • process.title 指定进程名称,有的时候需要给进程指定一个名�?

下面再稍微介绍下某些方法的使用:

process.cwd()

返回当前 Node 进程执行的目�?
一个 Node 模块 A 通过 NPM 发布,项�?B 中使用了模块 A。在 A 中需要操�?B 项目下的文件时,就可以用 process.cwd() 来获�?B 项目的路�?

process.argv

在终端通过 Node 执行命令的时候,通过 process.argv 可以获取传入的命令行参数,返回值是一个数组:

  • 0: Node 路径(一般用不到,直接忽略)
  • 1: 被执行的 JS 文件路径(一般用不到,直接忽略)
  • 2~n: 真实传入命令的参�?
    所以,我们只要�?process.argv[2] 开始获取就好了
1
const args = process.argv.slice(2);

process.env

返回一个对象,存储当前环境相关的所有信息,一般很少直接用到�?
一般我们会�?process.env 上挂载一些变量标识当前的环境。比如最常见的用 process.env.NODE_ENV 区分 development �?production

�?vue-cli 的源码中也经常会看到 process.env.VUE_CLI_DEBUG 标识当前是不�?DEBUG 模式

process.nextTick()

我们知道NodeJs是基于事件轮询,在这个过程中,同一时间只会处理一件事�?
在这种处理模式下,process.nextTick()就是定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行

例如下面例子将一个foo函数在下一个时间点调用

1
2
3
4
5
6
function foo() {
console.error('foo');
}

process.nextTick(foo);
console.error('bar');

输出结果为barfoo

虽然下述方式也能实现同样效果�?

1
2
setTimeout(foo, 0);
console.log('bar');

两者区别在于:

  • process.nextTick()会在这一次event loop的call stack清空后(下一次event loop开始前)再调用callback
  • setTimeout()是并不知道什么时候call stack清空的,所以何时调用callback函数是不确定�?

参考文�?

面试官:说说 Node 文件查找的优先级以及 Require 方法的文件查找策�?

一、模块规�?

NodeJSCommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:

  • 在Node中每一个js文件都是一个单独的模块
  • 模块中包括CommonJS规范的核心变量:exports、module.exports、require
  • 通过上述变量进行模块化开�?
    而模块化的核心是导出与导入,在Node中通过exportsmodule.exports负责对模块中的内容进行导出,通过require函数导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

二、查找策�?

require方法接收一下几种参数的传递:

  • 原生模块:http、fs、path�?- 相对路径的文件模块:./mod�?./mod
  • 绝对路径的文件模块:/pathtomodule/mod
  • 目录作为模块�?/dirname
  • 非原生模块的文件模块:mod

require参数较为简单,但是内部的加载却是十分复杂的,其加载优先级也各自不同,如下图�?

从上图可以看见,文件模块存在缓存区,寻找模块路径的时候都会优先从缓存中加载已经存在的模块

原生模块

而像原生模块这些,通过require 方法在解析文件名之后,优先检查模块是否在原生模块列表中,如果在则从原生模块中加载

绝对路径、相对路�?

如果require绝对路径的文件,则直接查找对应的路径,速度最�?
相对路径的模块则相对于当前调用require的文件去查找

如果按确切的文件名没有找到模块,�?NodeJs 会尝试带�?.js.json �?.node 拓展名再加载

目录作为模块

默认情况是根据根目录中package.json文件的main来指定目录模块,如:

1
2
{ "name" : "some-library",
"main" : "main.js" }

如果这是在./some-library node_modules目录中,�?require('./some-library') 会试图加�?./some-library/main.js

如果目录里没�?package.json文件,或�?main入口不存在或无法解析,则会试图加载目录下�?index.js �?index.node 文件

非原生模�?

在每个文件中都存在module.paths,表示模块的搜索路径,require就是根据其来寻找文件

window下输出如下:

1
2
[ 'c:\\nodejs\\node_modules',
'c:\\node_modules' ]

可以看出module path的生成规则为:从当前文件目录开始查找node_modules目录;然后依次进入父目录,查找父目录下的node_modules目录,依次迭代,直到根目录下的node_modules目录

当都找不到的时候,则会从系统NODE_PATH环境变量查找

举个例子�?

如果在/home/ry/projects/foo.js文件里调用了 require('bar.js'),则 Node.js 会按以下顺序查找�?

  • /home/ry/projects/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js

这使得程序本地化它们的依赖,避免它们产生冲突

三、总结

通过上面模块的文件查找策略之后,总结下文件查找的优先级:

  • 缓存的模块优先级最�?
  • 如果是内置模块,则直接返回,优先级仅次缓存的模块
  • 如果是绝对路�?/ 开头,则从根目录找
  • 如果是相对路�?./开头,则从当前require文件相对位置�?- 如果文件没有携带后缀,先从js、json、node按顺序查�?- 如果是目录,则根�?package.json的main属性值决定目录下入口文件,默认情况为 index.js
  • 如果文件为第三方模块,则会引�?node_modules 文件,如果不在当前仓库文件中,则自动从上级递归查找,直到根目录

参考文�?

面试官:React事件绑定的方式有哪些?区别?

一、是什�?

react应用中,事件名都是用小驼峰格式进行书写,例如onclick要改写成onClick

最简单的事件绑定如下�?

1
2
3
4
5
6
7
8
9
class ShowAlert extends React.Component {
showAlert() {
console.log("Hi");
}

render() {
return <button onClick={this.showAlert}>show</button>;
}
}

从上面可以看到,事件绑定的方法需要使用{}包住

上述的代码看似没有问题,但是当将处理函数输出代码换成console.log(this)的时候,点击按钮,则会发现控制台输出undefined

二、如何绑�?

为了解决上面正确输出this的问题,常见的绑定方式有如下�?

  • render方法中使用bind
  • render方法中使用箭头函�?- constructor中bind
  • 定义阶段使用箭头函数绑定

render方法中使用bind

如果使用一个类组件,在其中给某个组�?元素一个onClick属性,它现在并会自定绑定其this到当前组件,解决这个问题的方法是在事件函数后使用.bind(this)this绑定到当前组件中

1
2
3
4
5
6
7
8
9
10
class App extends React.Component {
handleClick() {
console.log('this > ', this);
}
render() {
return (
<div onClick={this.handleClick.bind(this)}>test</div>
)
}
}

这种方式在组件每次render渲染的时候,都会重新进行bind的操作,影响性能

render方法中使用箭头函�?

通过ES6的上下文来将this的指向绑定给当前组件,同样再每一次render的时候都会生成新的方法,影响性能

1
2
3
4
5
6
7
8
9
10
class App extends React.Component {
handleClick() {
console.log('this > ', this);
}
render() {
return (
<div onClick={e => this.handleClick(e)}>test</div>
)
}
}

constructor中bind

constructor中预先bind当前组件,可以避免在render操作中重复绑�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class App extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('this > ', this);
}
render() {
return (
<div onClick={this.handleClick}>test</div>
)
}
}

定义阶段使用箭头函数绑定

跟上述方式三一样,能够避免在render操作中重复绑定,实现也非常的简单,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
class App extends React.Component {
constructor(props) {
super(props);
}
handleClick = () => {
console.log('this > ', this);
}
render() {
return (
<div onClick={this.handleClick}>test</div>
)
}
}

三、区�?

上述四种方法的方式,区别主要如下�?

  • 编写方面:方式一、方式二写法简单,方式三的编写过于冗杂
  • 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实�?
    综合上述,方式四是最优的事件绑定方式

参考文�?

面试官:React构建组件的方式有哪些?区别?

一、是什�?

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式

React中,一个类、一个函数都可以视为一个组�?
之前文章中,我们了解到组件所存在的优势:

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简�?- 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

二、如何构�?

React目前来讲,组件的创建主要分成了三种方式:

  • 函数式创�?- 通过 React.createClass 方法创建
  • 继承 React.Component 创建

函数式创�?

React Hooks出来之前,函数式组件可以视为无状态组件,只负责根据传入的props来展示视图,不涉及对state状态的操作

大多数组件可以写为无状态组件,通过简单组合构建其他组�?
React中,通过函数简单创建组件的示例如下�?

1
2
3
function HelloComponent(props, /* context */) {
return <div>Hello {props.name}</div>
}

通过 React.createClass 方法创建

React.createClass是react刚开始推荐的创建组件的方式,目前这种创建方式已经不怎么用了

像上述通过函数式创建的组件的方式,最终会通过babel转化成React.createClass这种形式,转化成如下�?

1
2
3
4
5
6
7
8
function HelloComponent(props) /* context */{
return React.createElement(
"div",
null,
"Hello ",
props.name
);
}

由于上述的编写方式过于冗杂,目前基本上不使用�?

继承 React.Component 创建

同样在react hooks出来之前,有状态的组件只能通过继承React.Component这种形式进行创建

有状态的组件也就是组件内部存在维护的数据,在类创建的方式中通过this.state进行访问

当调用this.setState修改组件的状态时,组价会再次会调用render()方法进行重新渲染

通过继承React.Component创建一个时钟示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}

tick() {
this.setState(state => ({
seconds: state.seconds + 1
}));
}

componentDidMount() {
this.interval = setInterval(() => this.tick(), 1000);
}

componentWillUnmount() {
clearInterval(this.interval);
}

render() {
return (
<div>
Seconds: {this.state.seconds}
</div>
);
}
}

三、区�?

由于React.createClass 创建的方式过于冗杂,并不建议使用

而像函数式创建和类组件创建的区别主要在于需要创建的组件是否需要为有状态组件:

  • 对于一些无状态的组件创建,建议使用函数式创建的方�?
  • 由于react hooks的出现,函数式组件创建的组件通过使用hooks方法也能使之成为有状态组件,再加上目前推崇函数式编程,所以这里建议都使用函数式的方式来创建组�?
    在考虑组件的选择原则上,能用无状态组件则用无状态组�?

参考文�?

0%