一般的な 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印刷関連の操作と実装の詳細な説明

推薦する

アカウントとパスワードを記憶する機能を実現するVueの考え方とプロセス

目次実装のアイデアアカウント パスワードを保存する方法は 3 つあります。機能インターフェースアカウ...

IDEA は Docker プラグインを使用します (初心者向けチュートリアル)

目次例示する1. Dockerリモートアクセスを有効にする2. Dockerに接続する3. イメージ...

ろうそくを溶かす(水滴)サンプルコードを実現する純粋な CSS

成果を達成する実装のアイデアフィルターのコントラストとぼかしを利用して溶ける効果を実現します。親要素...

DockerでLNMPアーキテクチャを展開する方法

環境要件: IPホスト名192.168.1.1ノード1プロジェクト計画:コンテナネットワークセグメン...

フロントエンドJavaScriptの動作原理

目次1. JavaScript エンジンとは何ですか? 2. V8エンジン3. ランタイム環境4. ...

一般的な Nginx のテクニックと例の概要

1. 複数サーバーの優先順位たとえば、各サーバー ブロックがポート 80 をリッスンする場合、www...

srcまたはcss背景画像のurl値はbase64でエンコードされたコードです

ウェブ上の一部の画像の src または CSS 背景画像 URL の後に、data:image/pn...

乱数、文字列、日付、検証コード、UUIDを生成するMySQLメソッド

目次乱数を生成する0から1までの乱数を生成する指定された範囲内で乱数を生成します6桁のモバイル認証コ...

js 配列から重複を削除する 11 の方法

実際の業務や面接では、「配列の重複排除」の問題によく遭遇します。以下は、js を使用して実装された配...

ブラウザのCSS、JavaScript、背景画像のキャッシュをクリアする簡単な方法

実際のプロジェクト開発プロセスでは、ページがサーバーにアップロードされます。サーバーへの負荷を軽減し...

Linux の Makefile とは何ですか? どのように機能しますか?

この便利なツールでプログラムをより効率的に実行およびコンパイルしますMakefile は自動コンパイ...

Nodejs モジュール システムのソースコード分析

目次概要CommonJS 仕様Node の CommonJS 仕様の実装モジュールのエクスポートとイ...

Tomcat の 404 エラーの解決方法の詳細な説明

Tomcat テストで 404 問題が発生します。問題は次のとおりです。 HTTP ステータス 40...

Reactにおけるコンテキスト適用シナリオの分析

コンテキストの定義と目的コンテキストは、コンポーネント ツリーにプロパティを明示的に渡すことなく、コ...

Dockerコンテナがホストポートにアクセスできない場合の解決策

最近、仕事中に問題が発生しました。Docker コンテナがホストの redis にアクセスできず、t...