学校採用フロントエンド面接のよくある質問 [6] - NodeJS
前に書かれている:
インタビューの質問はすべて整理され、GitHub で共有されています。スターへようこそ。
アドレス: https://github.com/shadowings-zy/front-end-school-recruitment-question
NodeJS
Q: NodeJSのIOモデルの特徴は何ですか? マルチスレッド同期 IO との違いは何ですか?
NodeJS(正確にはjsの実行環境、つまりv8)のIOモデルは「シングルスレッド非同期ノンブロッキング」が特徴です。
ただし、複数のスレッドによる同期 IO には独自の長所と短所があるため、実際のアプリケーションのシナリオに応じて選択する必要があります。
従来の考え方では、非同期 IO の利点は、IO 自体が多くのリソースを消費する必要がないことですが、欠点は、非線形コードによってもたらされる複雑さとメンテナンスの理解の難しさにあり、マルチスレッド同期 IO の欠点は次のとおりです。パフォーマンス リソースのオーバーヘッドとスレッド管理の問題です。
したがって、同じマシン リソース内では、非同期 IO の同時実行性はマルチスレッド同期 IO の同時実行性よりも高くなければならないことは明らかですが、サーバー プログラム自体は間違いなく IO だけでなく、論理演算部分、つまり重い論理演算でも構成されています。それでもパフォーマンスに影響します。つまり、CPU 負荷の高いタスクによって js の実行がブロックされ、非同期 IO が処理されなくなり、ノードの応答時間に大きな影響を与えます。
つまり、ノードの IO モデルは、IO 集中型のタスクの処理により適しています。マルチスレッド同期 IO は、コンピューティング集中型のタスクに適しています。
Q: V8 エンジンのガベージ コレクション メカニズムは何ですか?
1. 再利用可能かどうかの判断方法
(1) クリアマークを
付ける 変数が環境に投入されると(関数内で変数が宣言されるなど)、その変数は「環境に投入される」とマークされます。論理的には、環境に入る変数によって占有されているメモリは、実行フローが対応する環境に入るたびに使用される可能性があるため、解放することはできません。そして、変数が環境から離れると、「環境から離れる」とマークされます。
具体的なアプローチ:
ガベージ コレクターは、実行時にメモリに格納されているすべての変数にマークを付けます (もちろん、任意のマーク付け方法を使用できます)。
次に、実行環境内の変数とその環境内の変数が参照する変数を削除します。
その後、マークが付いている変数は実行環境からアクセスできなくなるため、削除対象の変数とみなされます。
最後に、ガベージ コレクターはメモリのクリーンアップを完了し、マークされた値を破棄し、それらが占有していたメモリ領域を再利用します。
(2) 参照カウント 参照カウント
の意味は、各値が参照された回数を追跡して記録することです。
変数が宣言され、その変数に参照型の値が割り当てられている場合、この値の参照数は 1 です。
同じ値が別の変数に代入されている場合、その値の参照カウントは 1 増加します。
逆に、この値への参照を含む変数が別の値をとる場合、その値の参照カウントは 1 ずつ減らされます。
この値の参照カウントが 0 になると、その値が占有しているメモリ領域を再利用できるため、ガベージ コレクタが次回実行されるときに、参照カウントが 0 の値が占有しているメモリが解放されます。
しかし、これには循環参照の問題が発生します。
2. V8 ガベージ コレクション戦略は、
メモリを新世代と旧世代の 2 つの世代に分割します。
新世代のオブジェクトは生存時間の短いオブジェクトであり、旧世代のオブジェクトは生存時間の長いオブジェクトまたは常駐メモリです。効率を向上させるために、新世代と旧世代で異なるガベージ コレクション アルゴリズムが使用されます。新しい世代の世代は、特定の条件が満たされると古い世代に昇格します。
新しい世代では主に Scavenge を使用して管理します。メモリは均等に 2 つに分割されます。使用済み領域は From と呼ばれ、アイドル領域は To と呼ばれます。新しいオブジェクトは最初に From 領域に割り当てられ、残りのオブジェクトはスペースがいっぱいになりそうになったら To スペースに移動し、From メモリ スペースをクリアします。このとき、From スペースと To スペースを入れ替えてメモリの割り当てを続けます。昇格条件が満たされると、オブジェクトは、From メモリ スペースから昇格されます。新しい世代から古い世代へ。
オブジェクトの昇格には主に 2 つの条件があります。
オブジェクトが From スペースから To スペースに 2 回目にコピーされる場合、オブジェクトは古い世代に移動されます。
From スペースから To スペースにオブジェクトをコピーするときに、To スペースが 25% 以上使用されている場合、オブジェクトは古い世代に直接昇格されます。(しきい値を 25% に設定する理由は、Scavenge リカバリが完了すると、To 領域が From 領域となり、次のメモリ割り当てがこの領域で行われるためです。この比率が高すぎると、影響が及ぶ可能性があります。後続のメモリ割り当て)
旧世代では主に Mark-Sweet アルゴリズムと Mark-Compact アルゴリズムが使用され、1 つはマークの削除に、もう 1 つはマークの圧縮に使用されます。2 つの違いは、Mark-Sweet はガベージ コレクション後に断片化されたメモリを生成するのに対し、Mark-Compact はクリアする前にソートのステップを実行し、生き残ったオブジェクトを一方の側に移動してから、境界の反対側のメモリをクリアすることです。 , そのため無料です すべてのメモリは連続していますが、速度が遅くなるという問題があります。V8 では、古い世代は Mark-Sweet と Mark-Compact の両方によって共同管理されます。
Q: EventEmitter を実装しますか?
達成:
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
使用:
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: es6 モジュール化と commonjs モジュール化の違いは何ですか?
es6 のモジュール性:
在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のモジュール化:
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関連フレームワーク
Q: Koa のタマネギ モデルについて簡単に説明してください。
koa オニオン モデルは、koa の各ミドルウェアの実行順序を指します。
koa が複数のミドルウェアでロジックを実行する場合、最初に最初のミドルウェアのロジックを実行し、次に next() 関数を実行した後に 2 番目のミドルウェアのロジックを実行するというように、最後のミドルウェアまで実行されます。最後のミドルウェアが実行されると、最後から 2 番目のミドルウェア next() 関数の背後にあるコードが実行されるようにジャンプして戻り、最初のミドルウェア next() 関数の背後にあるコードが実行されるまで同様に実行されます。
例えば:
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}`)
http://localhost:3000 にアクセスすると、コンソールに次のように出力されます。
1
2
3
2
1