School recruitment front-end interview frequently asked questions [6] - NodeJS
Written in front:
All the interview questions have been sorted out and shared on GitHub. Welcome to star.
Address: https://github.com/shadowings-zy/front-end-school-recruitment-question
NodeJS
Q: What are the characteristics of the IO model of NodeJS? How is it different from multithreaded synchronous IO?
The IO model of NodeJS (more precisely, the execution environment of js, that is, v8) is characterized by "single-threaded asynchronous non-blocking".
However, synchronous IO with multiple threads has its own advantages and disadvantages, and it should be chosen according to the actual application scenario.
In the traditional view, the advantage of asynchronous IO is that IO itself does not need to take up too many resources. The disadvantage lies in the complexity brought by nonlinear code and the difficulty of understanding maintenance. The disadvantage of multi-threaded synchronous IO lies in the overhead of performance resources and Problems with thread management.
So obviously, in the same machine resources, the concurrency of asynchronous IO must be higher than that of multi-threaded synchronous IO; but the server program itself is definitely not only composed of IO, but also the logical operation part, the heavy logical operation Still affects performance. In other words, CPU-intensive tasks will block the execution of js, causing asynchronous IO to not be processed, which greatly affects the response time of node.
In short, node's IO model is more suitable for processing IO-intensive tasks. Multithreaded synchronous IO is more suitable for computing-intensive tasks.
Q: What is the garbage collection mechanism of the V8 engine?
1. How to judge whether it can be recycled
(1) Mark clear
When a variable enters the environment (for example, a variable is declared in a function), the variable is marked as "entering the environment". Logically, the memory occupied by variables that enter the environment can never be freed, since they may be used whenever the execution flow enters the corresponding environment. And when a variable leaves the environment, it is marked as "leaving the environment".
Specific approach:
The garbage collector will mark all variables stored in memory when it runs (of course, any marking method can be used).
Then, it will remove the variables in the running environment and the variables referenced by the variables in the environment.
After that, the variables that are still marked are regarded as variables to be deleted because they are no longer accessible in the running environment. .
Finally, the garbage collector completes the memory cleanup, destroying those marked values and reclaiming the memory space they occupied.
(2) Reference counting
The meaning of reference counting is to track and record the number of times each value is referenced.
When a variable is declared and a reference type value is assigned to the variable, the reference count of this value is 1.
If the same value is assigned to another variable, the reference count of the value is increased by 1.
Conversely, if the variable containing the reference to this value takes another value, the value's reference count is decremented by 1.
When the reference count of this value becomes 0, the memory space occupied by it can be reclaimed, so that when the garbage collector runs next time, it will release the memory occupied by the value with zero reference count.
But this will have the problem of circular reference.
2. The V8 garbage collection strategy
divides the memory into two generations: the new generation and the old generation.
The objects in the new generation are objects with a short survival time, and the objects in the old generation are objects with a long survival time or resident memory. Different garbage collection algorithms are used for the new and old generations to improve efficiency. The objects in the new generation will be promoted to the old generation after certain conditions are met.
The new generation mainly uses Scavenge for management. The memory is divided into two evenly. The used space is called From, and the idle space is called To. New objects are allocated to the From space first, and the surviving objects are copied to the To space when the space is about to be full. , and then clear the From memory space. At this time, swap the From space and To space and continue memory allocation. When the promotion conditions are met, the object will be promoted from the new generation to the old generation.
There are two main conditions for object promotion:
If an object is copied from the From space to the To space for the second time, the object will be moved to the old generation.
When copying an object from the From space to the To space, if the To space has been used for more than 25%, the object will be directly promoted to the old generation. (The reason for setting the threshold of 25% is that after the Scavenge recovery is completed, the To space will become From space, and the next memory allocation will be carried out in this space. If the ratio is too high, it will affect subsequent memory allocation )
The old generation mainly uses the Mark-Sweep and Mark-Compact algorithms, one for mark removal and one for mark compaction. The difference between the two is that Mark-Sweep will generate fragmented memory after garbage collection, while Mark-Compact will perform a step of sorting before clearing, moving surviving objects to one side, and then clearing the memory on the other side of the boundary, so that it is free All memory is continuous, but the problem is that the speed will be slower. In V8, the old generation is jointly managed by both Mark-Sweep and Mark-Compact.
Q: Implement an EventEmitter?
accomplish:
class EventEmitter {
constructor() {
this._events = {
}
}
subscribe(type, handler) {
if (this._events.hasOwnProperty(type)) {
this._events[type].push(handler)
} else {
this._events[type] = [handler]
}
}
unsubscribe(type, handler) {
if (this._events.hasOwnProperty(type)) {
const index = this._events[type].indexOf(handler)
if (index > -1) {
this._events[type].splice(index, 1)
}
}
}
once(type, handler) {
let fired = false
let _this = this
function magic() {
_this.unsubscribe(type, magic)
if (!fired) {
fired = true
handler.apply(_this, arguments)
}
}
this.subscribe(type, magic)
}
emit(type, args) {
if (this._events.hasOwnProperty(type)) {
this._events[type].forEach((fn) => fn(args))
}
}
}
module.exports = EventEmitter
use:
const EventEmitter = require('./myEventEmitter')
const eventEmitter = new EventEmitter()
const fn = (args) => {
console.log('good args', args)
}
const fn2 = (args) => {
console.log('good args 2', args)
}
const fn3 = (args) => {
console.log('good args 3', args)
}
eventEmitter.subscribe('good', fn)
eventEmitter.subscribe('good2', fn2)
eventEmitter.emit('good', 11111)
eventEmitter.emit('good2', 22222)
eventEmitter.unsubscribe('good', fn)
eventEmitter.emit('good2', 22222)
eventEmitter.once('good3', fn3)
eventEmitter.emit('good3', 33333)
eventEmitter.emit('good3', 33333)
Q: What is the difference between es6 modularization and commonjs modularization?
es6 modularity:
在es6规范中,使用import和export可以使js文件模块化。
每个import的js文件都是单例,如果再次import,就直接在内存中进行读取。
导出方式1:
//lib.js 文件
let foo = "stringFoo";
let fn0 = function() {
console.log("fn0");
};
export{foo, fn}
//main.js文件
import {foo, fn} from "./lib";
console.log(bar+"_"+foo);
commonjs modularization:
Node 应用由模块组成,采用 CommonJS 模块规范。
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。如果要定义全局变量,需要global属性。
CommonJS规范规定,每个模块内部,module变量代表当前模块。
这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
为了方便,Node为每个模块提供一个exports变量,指向module.exports。
例如:
var test = function () {
console.log(123);
};
module.exports.test = test;
使用require('XXX')加载模块。
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
NodeJS related framework
Q: Please briefly describe Koa's onion model?
The koa onion model refers to the execution order of each middleware in koa.
When koa executes the logic in multiple middleware, it will execute the logic of the first middleware first, and then execute the logic of the second middleware after executing the next() function, and so on until the last middleware. When the last middleware is executed, it will jump back to execute the code behind the next-to-last middleware next() function, and so on, until the code behind the first middleware next() function is executed.
for example:
const Koa = require('koa')
const app = new Koa()
const PORT = 3000
// #1
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(1)
})
// #2
app.use(async (ctx, next) => {
console.log(2)
await next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
})
app.listen(PORT)
console.log(`http://localhost:${
PORT}`)
Visit http://localhost:3000, the console prints:
1
2
3
2
1