Nodejs 面试一句话原理

103 阅读12分钟

Nodejs适用于哪些场景?

  1. 后端开发,Nodejs的异步I/O天生适合做Web高并发。
  2. BFF开发,比如SSR中间层或者GraphQL中间层。
  3. 前端基建,Webpack、Gulp、Babel、Jest等等前端工程化的工具或插件。

事务

// 在配置文件 config/plugin.js 中启用 egg-mysql 插件
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};

// 在 Controller 中使用事务
async function transactionExample() {
  const conn = await this.app.mysql.beginTransaction();
  try {
    // 第一个 SQL 语句
    await conn.query('INSERT INTO table1 SET ?', { name: 'Alice' });
    // 第二个 SQL 语句
    await conn.query('INSERT INTO table2 SET ?', { name: 'Bob' });
    // 提交事务
    await conn.commit();
  } catch (err) {
    // 发生错误时回滚事务
    await conn.rollback();
    throw err;
  } finally {
    // 释放连接
    await conn.release();
  }
}

Node.js 的事件循环包括以下几个主要的阶段:

  1. timers(定时器) :这个阶段负责处理定时器的回调函数,例如 setTimeout()setInterval() 创建的定时器。
  2. pending callbacks(待处理的回调) :这个阶段用于执行系统操作(例如 TCP 错误、处理错误类型)的回调函数。
  3. idle, prepare(空闲、准备) :这个阶段主要是内部使用的,通常不需要关注。
  4. poll(轮询) :这个阶段处理 I/O 事件,包括获取新的 I/O 事件,执行 I/O 相关的回调函数(例如网络请求、文件 I/O 等)。
  5. check(检查) :在此阶段,执行 setImmediate() 的回调函数。
  6. close callbacks(关闭回调) :在此阶段执行一些关闭的回调函数,例如 socket.on('close', ...)
console.log('start');  

setTimeout(() => {
  console.log('timeout');     
  process.nextTick(() => {
    console.log('nextTick1');  
  });
}, 1000);

setImmediate(() => {
  console.log('immediate');   
});

const fs = require('fs');
fs.readFile(__filename, () => {
  console.log('readFile');  
  
  Promise.resolve(123).then(() => {
    console.log('promise 4')
  })

  setImmediate(() => {
    console.log('immediate in readFile callback');
  });
  setTimeout(() => {
    console.log('timeout in readFile callback');
  }, 0);
});

Promise.resolve(123).then(() => {
    console.log('promise1')
})

Promise.resolve(123).then(() => {
    console.log('promise2')
})

setTimeout(() => {
    console.log('timeout 0');

    Promise.resolve(123).then(() => {
        console.log('promise 3')
    })
}, 0);

setImmediate(() => {
    console.log('immediate 0');
}, 0);

process.nextTick(() => {
  console.log('nextTick2');    
});

console.log('end');  


start
end
nextTick2
promise1
promise2
timeout 0
promise 3
immediate
immediate 0
readFile
promise 4
immediate in readFile callback
timeout in readFile callback
timeout
nextTick1

在这个案例中,有以下几个重要的步骤:

  1. 执行 console.log('Start'),输出 "Start"。
  2. 调用 setTimeout() 创建一个定时器,由于超时时间设置为 0,因此它会被添加到事件循环的"定时器"阶段。
  3. 执行两个 Promise 的 then() 方法,它们会添加到微任务队列中。
  4. 调用 fs.readFile() 执行文件 I/O 操作,该操作会被添加到事件循环的"轮询"阶段。
  5. 执行 console.log('End'),输出 "End"。
  6. 完成同步任务后,事件循环开始执行阶段,首先是"定时器"阶段。在这个阶段,定时器回调函数会被执行,输出 "Timeout 1"。
  7. 然后,事件循环会检查微任务队列,执行微任务。这时会输出 "Promise 1" 和 "Promise 2"。
  8. 最后,事件循环继续执行"轮询"阶段,处理文件 I/O 操作,当文件读取完成后,会触发回调函数,输出 "File read"。

**宏任务(macrotasks)**是指在事件循环中需要执行的任务,这些任务通常包括定时器回调函数、I/O 操作、UI 渲染等。在 Node.js 中,每个事件循环阶段都可以包含一个或多个宏任务。

**微任务(microtasks)**则是在宏任务执行完毕之后立即执行的任务,它们比宏任务具有更高的优先级。典型的微任务包括 Promise 的回调函数(then()catch() 方法中的回调)和 process.nextTick() 等。微任务通常用于执行一些需要立即响应的操作,例如更新界面状态、执行后续的逻辑等。

Buffer

在 Node.js 中,Buffer 是一个用于处理二进制数据的对象。Node.js 的设计目标之一是支持高性能的网络和文件 I/O 操作,而处理二进制数据是这些操作中的重要一部分。以下是 Node.js 中使用 Buffer 的几个原因:

  1. 性能:Buffer 对象在处理二进制数据时非常高效。它底层是使用 C++ 实现的,可以直接操作底层的内存,避免了 JavaScript 中频繁的数据拷贝操作。这使得 Node.js 能够快速地处理大量的二进制数据,例如在网络传输、文件 I/O 等场景下。
  2. 内存分配:Buffer 对象可以预先分配一块内存空间,然后通过填充、复制等操作来处理数据,这种方式比动态分配内存更加高效。在一些需要频繁创建和销毁二进制数据的场景下,Buffer 可以减少内存分配和释放的开销。
  3. 二进制数据处理:Node.js 应用程序通常需要处理各种形式的二进制数据,例如图像、音频、视频等。Buffer 提供了一组方法来操作和处理这些二进制数据,包括读取、写入、拼接、截取等,使得开发者能够方便地进行数据处理。
  4. 与底层系统交互:在 Node.js 中,与操作系统进行交互的许多 API 都是基于二进制数据的,例如文件 I/O、网络通信等。Buffer 提供了与这些底层 API 进行交互的接口,使得 Node.js 能够直接操作二进制数据,并且与操作系统进行高效的交互。

二进制数据比文本数据效率更高的主要原因有几个方面:

  1. 内存占用: 二进制数据通常比文本数据更紧凑,因为二进制数据是直接以字节的形式存储,而文本数据则需要考虑字符编码的影响。例如,在文本数据中,一个字符可能需要多个字节来表示,而在二进制数据中,每个字节都可以直接表示一定范围的值,因此在存储相同数量的信息时,二进制数据通常会占用更少的内存空间。
  2. 处理速度: 二进制数据的处理速度通常比文本数据更快,因为在处理二进制数据时,无需考虑字符编码和字符集的转换,也不需要进行字符串的拆分和连接操作,而是直接对字节进行操作。此外,很多硬件和软件平台都对二进制数据进行了优化,提供了更高效的处理方式。
  3. 数据传输: 二进制数据在网络传输和存储时通常比文本数据更高效。因为二进制数据不需要进行字符编码和解码,可以直接以字节的形式进行传输和存储,而文本数据则需要考虑字符编码的转换和解析,这会增加数据传输和存储的开销。

Koa中间件原理了解吗?

Koa洋葱圈中间件实现原理主要有以下两点:

  1. 数组里面存函数:使用middleware来存储中间函数。
javascript
复制代码
use (fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
  debug('use %s', fn._name || fn.name || '-')
  this.middleware.push(fn)
  return this
}
  1. compose函数:将一组中间件函数组合成一个大的异步函数。这个大的异步函数会依次执行每个中间件函数,并将每个中间件函数的执行结果传递给下一个中间件函数。最终,这个大的异步函数会返回一个Promise对象,表示整个中间件链的执行结果。
javascript
复制代码
function compose(middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    let index = -1
    return dispatch(0)

    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

什么是Stream流,有哪些应用场景?

Stream是一种处理流式数据的抽象接口,用于读取、写入、转换和操作数据流。它是一个基于事件的 API,可以让我们以高效、低延迟的方式处理大型数据集。

说直白点就是基于Stream封装的API,性能更好。

比如读取文件,使用流我们可以一点一点来读取文件,每次只读取或写入文件的一小部分数据块,而不是一次性将整个文件读取或写入到内存中或磁盘中,这样做能够降低内存占用。

有没有了解过Redis?

可以从以下方面来回答:

  1. 用Redis实现缓存:将热门数据和热门页面存到Redis进行缓存,比如热门商品信息,商品页面和网站首页。
  2. 缓存遇到的问题:缓存穿透、缓存雪崩、缓存击穿。
  3. Redis的进阶功能:Redis有各种数据结构,除了缓存之外,还能实现很多功能。比如:消息队列、附近的人、排行榜等等。
  4. Redis持久化:Redis可以将缓存持久化到本地,持久化策略包括RDB和AOF。
  5. 集群:如果单机Redis不够用的话,可以考虑搭建Redis集群,Redis集群有主从和哨兵两种模式。

Redis对于后端来说,是一个专门的话题,我将会在我的后端面试手册中详细讲解,感兴趣的小伙伴可以持续关注。

什么是ORM?Nodejs的ORM框架有哪些?

ORM框架是通过对SQL语句进行封装,并将数据库的数据表和用户代码里的模型对象进行自动映射。

有没有做过数据库优化?

常见的优化有:

  1. 使用explain执行计划查看SQL的执行信息,进而定位慢SQL来源。
  2. 索引是Mysql调优首先能想到的方案,合理设置索引可以很大程度上提高查询效率。
  3. 大分页也是一个常见的性能问题出现的地方,因为MySQL需要扫描大量的数据,造成性能瓶颈。可以通过使用主键或者游标分页的方式来优化。
  4. 读写分离,单机顶不住的时候,可以使用主从架构,把数据库读写分担到不同的机器上。
  5. 分库分表,如果数据表存了海量数据,除了读写分离之外,还要考虑分库分表,把一张表分成多张表,减轻数据库压力。

假设我们有一个社交网络应用,其中有用户发布动态(写操作)和查看动态(读操作)两种操作。我们将通过引入读写分离来优化这个应用。

变化前:

javascriptCopy code
// 单一数据库连接
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/social_network', { useNewUrlParser: true, useUnifiedTopology: true });

// 用户模型
const User = mongoose.model('User', { name: String, email: String });

// 创建用户
const createUser = async (name, email) => {
    const user = new User({ name, email });
    await user.save();
    console.log('User created:', user);
};

// 获取所有用户
const getAllUsers = async () => {
    const users = await User.find();
    console.log('All users:', users);
};

// 示例操作
createUser('John', 'john@example.com');
getAllUsers();

变化后:

javascriptCopy code
// 主从数据库连接
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/social_network_master', { useNewUrlParser: true, useUnifiedTopology: true });

// 从数据库连接
const mongooseSlave = require('mongoose');
mongooseSlave.connect('mongodb://localhost:27017/social_network_slave', { useNewUrlParser: true, useUnifiedTopology: true });

// 用户模型
const User = mongoose.model('User', { name: String, email: String });

// 创建用户(写操作)
const createUser = async (name, email) => {
    const user = new User({ name, email });
    await user.save();
    console.log('User created:', user);
};

// 获取所有用户(读操作)
const getAllUsers = async () => {
    const users = await User.find().read('secondary');
    console.log('All users:', users);
};

// 示例操作
createUser('John', 'john@example.com');
getAllUsers();

在变化后的代码中,我们引入了一个从数据库连接,并且在执行读操作时指定了使用从数据库。这样,写操作将集中在主数据库,而读操作则会分担到从数据库,实现了读写分离。

分库分表

在分库分表的场景下,你需要根据业务需求来确定要检索哪个表。一般来说,你可能会遵循一定的规则或策略来选择要检索的表,这些规则可能包括:

  1. 数据分片规则:根据数据的某些属性(例如用户ID、时间范围等)来选择要检索的表。例如,你可以根据用户ID来决定检索哪个用户的数据,或者根据时间范围来选择检索哪个时间段内的数据。
  2. 性能优化策略:根据表的特性和性能指标来选择要检索的表。例如,你可能会根据表的大小、索引情况、存储引擎等因素来选择要检索的表,以提高查询效率和减轻数据库压力。
  3. 负载均衡策略:根据数据库服务器的负载情况来选择要检索的表。例如,你可以根据不同数据库服务器的负载情况来动态选择要检索的表,以实现负载均衡和资源优化。

在实际应用中,通常会根据以上策略来动态地选择要检索的表。你可能需要在代码中实现这些策略,并根据具体的查询条件和业务需求来确定要检索的表。

有了解过分布式和微服务吗?

当单体应用撑不住的时候,就得考虑上集群,把应用部署在多个机器上,就形成了分布式架构。

分布式的集群不仅带来了算力和并发能力,也带来了各种问题,这其中包括:分布式通信、分布式事务、分布式id、分布式容错、负载均衡等。

所以就需要有各种中间件来解决这些问题,比如Nginx、Zookeeper、Dubbo、MQ、RPC等。

然后当项目规模进一步扩大的时候,不仅要考虑集群,还要考虑项目的拆分,这时候就要上微服务架构了。把一个大项目根据业务拆分成很多功能单一的模块,可以由不同的团队独立开发和部署。

比如一个电商的后台API,可以拆分成用户服务、商品服务、订单服务、优惠券服务、广告服务,这些服务由不同的团队去维护。

当然,微服务也带来了更多的复杂性,所以就会有像Spring Cloud、Spring Cloud Alibaba这样的解决方案去解决这些复杂性。