一般的な JavaScript メモリ エラーと解決策

一般的な JavaScript メモリ エラーと解決策

序文:

JavaScriptではメモリ管理操作は提供されません。代わりに、メモリはガベージ コレクションと呼ばれるメモリ再利用プロセスを通じてJavaScript VMによって管理されます。

ガベージ コレクションを強制することはできないので、それが機能するかどうかをどうやって確認すればよいのでしょうか。また、ガベージ コレクションについて何がわかっているのでしょうか。

このプロセス中、スクリプトの実行は一時停止されます。 アクセスできないリソースのメモリが解放されます。 非決定論的です。 一度にメモリ全体をチェックするのではなく、複数のサイクルで実行されます。 予測できませんが、必要なときに実行されます。 これは、リソースとメモリの割り当ての問題について心配する必要がないことを意味しますか? もちろん違います。注意しないと、メモリリークが発生する可能性があります。

メモリリークとは何ですか?

メモリ リークとは、ソフトウェアが再利用できない割り当てられたメモリのブロックです。

Javascriptガベージコレクターを提供しますが、メモリリークを回避できるわけではありません。ガベージ コレクションの対象となるには、オブジェクトが他の場所から参照されないようにする必要があります。未使用のリソースへの参照が保持されている場合、それらのリソースは収集されなくなります。これを無意識の記憶保持といいます。

メモリがリークすると、ガベージ コレクターがより頻繁に実行される可能性があります。このプロセスによりスクリプトの実行が妨げられるため、プログラムが停止する可能性があります。停止すると、うるさいユーザーは間違いなくそれに気づき、不満を抱くと、製品はすぐにオフラインになります。さらに深刻なケースでは、アプリケーション全体がクラッシュし、gg になります。

メモリ リークを防ぐにはどうすればよいでしょうか。重要なのは、不要なリソースを保持しないようにすることです。一般的なシナリオをいくつか見てみましょう。

1. タイマー監視

setInterval()メソッドは、各呼び出しの間に一定の時間遅延を設けて、関数を呼び出したり、コード スニペットを繰り返し実行したりします。間隔を一意に識別する間隔 ID を返すので、後でclearInterval()を呼び出して間隔を削除できます。

x 回のループ後に完了したことを通知するコールバック関数を呼び出すコンポーネントを作成します。この例ではReactを使用していますが、これはどの FE フレームワークにも当てはまります。

React をインポートし、{useRef} を 'react' から取得します。 
 
const タイマー = ({ cicles, onFinish }) => { 
    定数 currentCicles = useRef(0); 
 
    間隔を設定する(() => { 
        (currentCicles.current >= cicles) の場合 { 
            終了時に(); 
            戻る; 
        } 
        currentCicles.current++; 
    }, 500); 
 
    戻る ( 
        <div>読み込み中...</div> 
    ); 
} 
 
デフォルトのタイマーをエクスポートします。 


一見、問題はないように見えます。心配しないでください。このタイマーをトリガーする別のコンポーネントを作成し、そのメモリ パフォーマンスを分析しましょう。

React をインポートし、{useState} を 'react' から取得します。 
'../styles/Home.module.css' からスタイルをインポートします 
'../components/Timer' から Timer をインポートします。 
 
デフォルト関数 Home() をエクスポートします。 
    const [showTimer、setShowTimer] = useState(); 
    const onFinish = () => setShowTimer(false); 
 
    戻る ( 
      <div className={styles.container}> 
          {表示タイマー? ( 
              <タイマーサイクル={10} onFinish={onFinish} /> 
          ): ( 
              <ボタンのクリック時={() => setShowTimer(true)}> 
                リトライ 
              </ボタン> 
          )} 
      </div> 
    ) 
} 


「再試行」ボタンを数回クリックした後、 Chrome Dev Toolsを使用してメモリ使用量を取得した結果は次のとおりです。

「再試行」ボタンをクリックすると、割り当てられるメモリがどんどん増えていくのがわかります。これは、以前に割り当てられたメモリが解放されていないことを意味します。タイマーは交換されずにまだ動作しています。

この問題を解決するにはどうすればよいでしょうか? setInterval の戻り値は間隔 ID であり、これを使用して間隔をキャンセルできます。この特別なケースでは、コンポーネントがアンマウントされた後にclearIntervalを呼び出すことができます。

使用効果(() => { 
    定数intervalId = setInterval(() => { 
        (currentCicles.current >= cicles) の場合 { 
            終了時に(); 
            戻る; 
        } 
        currentCicles.current++; 
    }, 500); 
 
    戻り値 () => clearInterval(intervalId); 
}, []) 


コードを書いているときにこの問題を見つけるのは難しい場合があります。最善の方法は、コンポーネントを抽象化することです。

ここでReactを使用すると、このロジックすべてをカスタムHookでラップできます。

'react' から useEffect をインポートします。 
 
エクスポートconst useTimeout = (refreshCycle = 100, コールバック) => { 
    使用効果(() => { 
        リフレッシュサイクル <= 0 の場合 
            setTimeout(コールバック、0); 
            戻る; 
        } 
 
        定数intervalId = setInterval(() => { 
            折り返し電話(); 
        }, リフレッシュサイクル); 
 
        戻り値 () => clearInterval(intervalId); 
    }, [リフレッシュサイクル、設定間隔、クリア間隔]); 
}; 
 
デフォルトの useTimeout をエクスポートします。 


これで、setInterval を使用する必要があるときはいつでも、次のように実行できます。

const handleTimeout = () => ...; 
 
タイムアウトを使用します(100、ハンドルタイムアウト); 


これで、メモリ リークを心配せずにこのuseTimeout Hookを使用できるようになりました。これも抽象化の利点です。

2. イベント監視

Web API多数のイベント リスナーを提供します。先ほど、 setTimeoutについて説明しました。それでは、 addEventListenerを見てみましょう。

この例では、キーボード ショートカット機能を作成します。ページごとに異なる機能があるため、異なるショートカットキー機能を作成します。

関数 homeShortcuts({ key}) { 
    if (キー === 'E') { 
        console.log('ウィジェットを編集') 
    } 
} 
 
// ユーザーがホームページにログインすると、document.addEventListener('keyup', homeShortcuts); が実行されます。  
 
 
// ユーザーが何か操作をしてから設定へ移動します function settingsShortcuts({ key}) { 
    if (キー === 'E') { 
        console.log('設定を編集') 
    } 
} 
 
// ユーザーがホームページにログインすると、document.addEventListener('keyup', settingsShortcuts); が実行されます。  


2 番目のaddEventListenerが実行されたときに前のkeyupがクリーンアップされないことを除けば、まだ問題ないように見えます。このコードは、 keyupリスナーを置き換える代わりに、別のcallbackを追加します。つまり、キーが押されると、2 つの機能がトリガーされます。

以前のコールバックをクリアするには、removeEventListener を使用する必要があります。

document.removeEventListener('keyup', homeShortcuts); 


上記のコードをリファクタリングします。

関数 homeShortcuts({ key}) { 
    if (キー === 'E') { 
        console.log('ウィジェットを編集') 
    } 
} 
 
// ユーザーがホームに戻り、 
document.addEventListener('keyup', homeShortcuts);  
 
 
// ユーザーが何か操作して設定に移動します 
 
関数設定ショートカット({キー}) { 
    if (キー === 'E') { 
        console.log('設定を編集') 
    } 
} 
 
// ユーザーがホームに戻り、 
document.removeEventListener('keyup', homeShortcuts);  
document.addEventListener('keyup', 設定ショートカット); 


原則として、グローバル オブジェクトのツールを使用する場合は十分に注意してください。

3.オブザーバー

Observers 、多くの開発者が認識していないブラウザWeb API機能です。これは、HTML 要素の可視性やサイズの変更を確認する場合に非常に便利です。

IntersectionObserverインターフェイス (Intersection Observer API に従属) は、ターゲット要素とその祖先要素または最上位のドキュメント ウィンドウ (ビューポート) との交差状態を非同期的に監視する方法を提供します。祖先要素とビューポートはルートと呼ばれます。

強力ではありますが、注意して使用する必要があります。オブジェクトの観察が終わったら、使用していないときは必ずキャンセルしてください。

コードを見てみましょう:

const ref = ... 
const 可視 = (可視) => { 
  console.log(`${visible} です`); 
} 
 
使用効果(() => { 
    (!参照)の場合{ 
        戻る; 
    } 
 
    オブザーバー.current = 新しい IntersectionObserver( 
        (エントリ) => { 
            エントリ[0]が交差している場合 
                表示される(true); 
            } それ以外 { 
                可視(false); 
            } 
        }, 
        { ルートマージン: `-${header.height}px` }, 
    ); 
 
    オブザーバー.current.observe(ref); 
}, [参照]); 


上記のコードは良さそうです。しかし、コンポーネントがアンマウントされると、オブザーバーはどうなるでしょうか? クリアされず、メモリ リークが発生します。

この問題をどうやって解決するのでしょうか? 切断メソッドを使用するだけです:

const ref = ... 
const 可視 = (可視) => { 
  console.log(`${visible} です`); 
} 
 
使用効果(() => { 
    (!参照)の場合{ 
        戻る; 
    } 
 
    オブザーバー.current = 新しい IntersectionObserver( 
        (エントリ) => { 
            エントリ[0]が交差している場合 
                表示される(true); 
            } それ以外 { 
                可視(false); 
            } 
        }, 
        { ルートマージン: `-${header.height}px` }, 
    ); 
 
    オブザーバー.current.observe(ref); 
 
    戻り値 () => observer.current?.disconnect(); 
}, [参照]); 


4. ウィンドウオブジェクト

Windowにオブジェクトを追加するのはよくある間違いです。シナリオによっては、特にWindow Executionコンテキストで this キーワードを使用する場合、見つけるのが難しい場合があります。

次の例を見てください。

関数 addElement(要素) { 
    if (!this.stack) { 
        this.stack = { 
            要素: [] 
        } 
    } 
 
    this.stack.elements.push(要素); 
} 


無害に見えますが、 addElement呼び出すコンテキストによって異なります。 Window ContextからaddElementを呼び出すと、パイルが大きくなります。

別の問題としては、グローバル変数を誤って定義している可能性があります。

var a = 'example 1'; // var が作成された場所にスコープが設定されます b = 'example 2'; // Window オブジェクトに追加されます

この問題を防ぐには、厳密モードを使用します。

「厳密な使用」 


厳密モードを使用すると、 JavaScriptコンパイラに対して、これらの動作から保護したいという指示を与えることになります。必要な場合には、引き続きWindowを使用できます。ただし、明示的に使用する必要があります。

厳密モードが前述の例に与える影響:

  • addElement 関数の場合、グローバル スコープから呼び出されると、これは未定義になります。
  • 変数に const | let | var を指定しないと、次のエラーが発生します。
キャッチされない参照エラー: b が定義されていません 


5. DOM参照を保持する

DOM ノードもメモリ リークの影響を受けません。それらへの参照を保存しないように注意する必要があります。そうしないと、まだアクセス可能なため、ガベージ コレクターはそれらをクリーンアップできません。

小さなコード スニペットで説明しましょう。

定数要素 = []; 
const リスト = document.getElementById('リスト'); 
 
関数addElement() { 
    // クリーンノード 
    リスト.innerHTML = ''; 
 
    divElement を document.createElement('div') に設定します。 
    const element = document.createTextNode(`要素 ${elements.length} を追加`); 
    divElement.appendChild(要素); 
 
 
    リストに子要素を追加します。 
    divElement をプッシュします。 
} 
 
document.getElementById('addElement').onclick = addElement; 


注: addElement 関数はリスト div をクリアし、その子として新しい要素を追加します。新しく作成された要素は、要素配列に追加されます。

次にaddElementが実行されると、要素はリスト div から削除されますが、 elements配列に格納されているため、ガベージ コレクションの対象にはなりません。

関数を数回実行した後、監視します。


上のスクリーンショットでノードがどのようにリークされているかを確認してください。では、これをどのように修正するのでしょうか? elements配列をクリアすると、ガベージ コレクションの対象になります。

要約:

この記事では、最も一般的なタイプのメモリ リークについて説明しました。明らかに、 JavaScript自体はメモリをリークしません。むしろ、これは開発者側の意図しないメモリ保持によって発生します。コードがクリーンであり、後からクリーンアップすることを忘れない限り、リークは発生しません。

JavaScriptでメモリとガベージコレクションがどのように機能するかを理解することは必須です。開発者の中には、自動なので心配する必要はないと誤解している人もいます。

一般的なJavaScriptメモリ エラーに関するこの記事はこれで終わりです。JavaScript メモリ エラーの詳細については、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • 一般的な JavaScript メモリ エラーと解決策
  • JavaScript の誤解: メモリ管理を気にしない

<<:  Linux でファイル権限を変更する chmod コマンドの詳細な分析

>>:  HTML印刷関連の操作と実装の詳細な説明

推薦する

MySQL の undo、redo、binlog の違いを簡単に分析します

目次序文【ログ取り消し】 【REDOログ】 【バイナリログ】要約する序文MySQL には、REDO ...

nestjs における例外フィルター Exceptionfilter の具体的な使用法

Nestjs 例外フィルターといえば、非常に強力な .Net のグローバル フィルターについて触れな...

docker run によって起動されたコンテナがハングしてデータが失われた場合の対処方法

シナリオの説明あるシステムでは、機能サービスはdocker stack deploy xxxで起動し...

CentOS7 環境で gcc (バージョン 10.2.0) をアップグレードする詳細な手順

目次簡単な紹介1. 現在のgccバージョンを確認する2. gccインストールパッケージ(バージョン1...

シンプルなカレンダー効果を実現する js

この記事では、シンプルなカレンダー効果を実現するためのjsの具体的なコードを参考までに共有します。具...

JavaScript で矢印関数を使用できないシナリオはどれですか

目次1. オブジェクトメソッドを定義する2. プロトタイプメソッドを定義する3. イベントコールバッ...

Webフォーム作成スキル

実際、上記の 3 つの表はいずれも 3 行 3 列です。区切り線を非表示にするコツはルールにあります...

202 無料の高品質 XHTML テンプレート (1)

ここで 123WORDPRESS.COM はこれらのテンプレートの最初の部分を紹介します。各テンプレ...

ローカル写真をアップロードする前にプレビューコード例を実装するための HTML5 と jQuery

HTML5 と jQuery はアップロード前にローカル画像のプレビューを実装しており、その効果は...

Linux プロセスが占有するポート番号を表示する 6 つの方法

Linux システム管理者にとって、サービスがポートに正しくバインドされているか、またはポートをリッ...

Reactにおける制御されたコンポーネントと制御されていないコンポーネントの簡単な分析

目次制御されていないコンポーネント制御コンポーネント知らせ結論は制御されていないコンポーネントフォー...

スプライトとフォントアイコンを実装するためのCSS

スプライト:以前は、各画像リソースは独立した画像でした。ブラウザが Web サイト内のさまざまな W...

Web ページは何ピクセルで設計すればよいでしょうか?

多くのウェブデザイナーは、ウェブページのレイアウトを設計する際に、インターフェースウェブページの幅に...

http-proxy-middlewareを使用してNodeでプロキシクロスドメインを実装する方法と手順

目次1. プロキシモジュールをインストールする2. プロキシを設定する1. プロキシモジュールをイン...

MySQL 8.0.15 winx64 解凍版のインストールと設定方法のグラフィックチュートリアル

この記事では、MySQL 8.0.15 winx64解凍版のインストールと設定方法を紹介します。具体...