熟練度コースへのフロントエンド (vue) エントリー: JavaScript を学習するためにエントリーすると、メモリ管理操作は提供されません。代わりに、メモリは、ガベージ コレクションと呼ばれるメモリ再利用プロセスを通じて JavaScript VM によって管理されます。
ガベージ コレクションを強制することはできないので、ガベージ コレクションが機能していることをどのように確認すればよいでしょうか?私たちはそれについてどれくらい知っていますか?
このプロセス中はスクリプトの実行が一時停止されます
アクセスできないリソースのメモリを解放します。
それは不確実です
メモリ全体を一度にチェックするのではなく、複数のサイクルで実行します。
予測不可能ですが、必要に応じて実行されます
これは、リソースとメモリの割り当ての問題を心配する必要がないことを意味しますか? もちろんそうではありません。注意しないと、メモリ リークが発生する可能性があります。
メモリ リークとは、ソフトウェアが再利用できない割り当てられたメモリのブロックです。
Javascript はガベージ コレクターを提供しますが、だからといってメモリ リークを回避できるわけではありません。ガベージ コレクションの対象となるには、オブジェクトが他の場所で参照されていてはなりません。未使用のリソースへの参照を保持すると、それらのリソースが再利用されなくなります。これは無意識の記憶保持と呼ばれます。
メモリ リークにより、ガベージ コレクタがより頻繁に実行される可能性があります。このプロセスによりスクリプトの実行が妨げられるため、プログラムがフリーズする可能性があります。このような遅延が発生すると、うるさいユーザーは、これに満足できない場合、製品が長期間オフラインになることに間違いなく気づきます。さらに深刻なことに、アプリケーション全体がクラッシュする可能性があります。これは gg です。
メモリリークを防ぐにはどうすればよいでしょうか? 重要なことは、不要なリソースを保持しないようにすることです。いくつかの一般的なシナリオを見てみましょう。
setInterval()
メソッドは、各呼び出しの間に固定の時間遅延を設けて、関数を繰り返し呼び出すか、コード フラグメントを実行します。間隔を一意に識別する間隔ID
ID
返されるため、後でclearInterval()
呼び出して間隔を削除できます。
コールバック関数を呼び出して、 x
のループ後に終了したことを示すコンポーネントを作成します。この例では React を使用していますが、これはどの FE フレームワークでも機能します。
import React, { useRef } from 'react'; const タイマー = ({ cicles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); 戻る; } currentCicles.current++; }, 500); 戻る ( <p>読み込み中...</p> ); } デフォルトのタイマーをエクスポートします。
一見すると何の問題もないように見えます。心配しないで、このタイマーをトリガーする別のコンポーネントを作成し、そのメモリ パフォーマンスを分析しましょう。
import React, { useState } from 'react'; 「../styles/Home.module.css」からスタイルをインポートします '../components/Timer' からタイマーをインポートします。 デフォルト関数 Home() をエクスポート { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); 戻る ( <p className={styles.container}> {ショータイマー? <タイマー cicles={10} onFinish={onFinish} /> ):( <button onClick={() => setShowTimer(true)}> リトライ </ボタン> )} </p> ) }
Retry
ボタンを数回クリックした後、Chrome デベロッパー ツールを使用してメモリ使用量を取得した結果は次のとおりです。
再試行ボタンをクリックすると、より多くのメモリが割り当てられることがわかります。これは、以前に割り当てられたメモリが解放されていないことを意味します。タイマーは交換されずにまだ動作しています。
この問題を解決するにはどうすればよいでしょうか? setInterval
の戻り値は間隔 ID であり、この間隔をキャンセルするために使用できます。この特定のケースでは、コンポーネントがアンロードされた後に、 clearInterval
を呼び出すことができます。
useEffect(() => { const intervalId = setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); 戻る; } currentCicles.current++; }, 500); return () => ClearInterval(intervalId); }、[])
コードを作成するときにこの問題を見つけるのが難しい場合があります。最善の方法は、コンポーネントを抽象化することです。
ここで React を使用すると、このすべてのロジックをカスタム フックにラップできます。
import { useEffect } から 'react'; エクスポート const useTimeout = (refreshCycle = 100、コールバック) => { useEffect(() => { if (refreshCycle <= 0) { setTimeout(コールバック, 0); 戻る; } const intervalId = setInterval(() => { 折り返し電話(); }、refreshCycle); return () => ClearInterval(intervalId); }, [refreshCycle、setInterval、clearInterval]); }; デフォルトの useTimeout をエクスポートします。
これで、 setInterval
使用する必要があるときはいつでも、これを行うことができます。
const handleTimeout = () => ...; useTimeout(100, handleTimeout);
これで、メモリ リークを気にせずにこのuseTimeout Hook
を使用できるようになり、これも抽象化の利点です。
Web API は多数のイベント リスナーを提供します。前に、 setTimeout
について説明しました。次に、 addEventListener
を見てみましょう。
この例では、キーボード ショートカット関数を作成します。ページごとに異なる機能があるため、異なるショートカット キー機能が作成されます。
関数 homeShortcuts({ key}) { if (key === 'E') { console.log('ウィジェットの編集') } } // ユーザーがホームページにログインすると、document.addEventListener('keyup', homeShortcuts); が実行されます。 // ユーザーが何かを行った後、設定関数に移動します settingsShortcuts({ key}) { if (key === 'E') { console.log('設定の編集') } } // ユーザーがホームページにログインすると、document.addEventListener('keyup', settingsShortcuts); を実行します。
2 番目のaddEventListener
の実行時に前のkeyup
がクリーンアップされないことを除けば、まだ問題ないようです。このコードはkeyup
リスナーを置き換えるのではなく、別のcallback
を追加します。これは、キーが押されると 2 つの機能がトリガーされることを意味します。
以前のコールバックをクリアするには、 removeEventListener
使用する必要があります。
document.removeEventListener('keyup', homeShortcuts);
上記のコードをリファクタリングします。
関数 homeShortcuts({ key}) { if (key === 'E') { console.log('ウィジェットの編集') } } // ユーザーがホームに着くと実行します document.addEventListener('keyup', homeShortcuts); // ユーザーは何らかの処理を行い、設定に移動します 関数設定ショートカット({ key}) { if (key === 'E') { console.log('設定の編集') } } // ユーザーがホームに着くと実行します document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', settingsShortcuts);
経験則として、グローバル オブジェクトのツールを使用する場合は十分に注意してください。
オブザーバーは、多くの開発者が気づいていないブラウザー Web API 機能です。これは、HTML 要素の可視性やサイズの変更を確認する場合に強力です。
IntersectionObserver
インターフェイス (Intersection Observer API の一部) は、ターゲット要素とその祖先要素またはトップレベルのドキュメントviewport
との交差ステータスを非同期的に監視するためのメソッドを提供します。祖先要素とviewport
root
と呼ばれます。
強力ではありますが、使用には注意が必要です。オブジェクトの観察が終了したら、使用していないときは忘れずにキャンセルしてください。
コードを見てください。
const ref = ... const 可視 = (可視) => { console.log(`${visible}` です); } useEffect(() => { if (!ref) { 戻る; } observer.current = 新しい IntersectionObserver( (エントリ) => { if (!entries[0].isIntersecting) { 可視(真); } それ以外 { 可視(偽); } }、 { rootMargin: `-${header.height}px` }, ); オブザーバー.current.observe(ref); }, [参照]);
上記のコードは問題ないようです。しかし、コンポーネントがアンロードされるとオブザーバーはどうなるでしょうか? クリアされず、メモリがリークされます。この問題を解決するには、次のようにdisconnect
メソッドを使用します。
const ref = ... const 可視 = (可視) => { console.log(`${visible}` です); } useEffect(() => { if (!ref) { 戻る; } observer.current = 新しい IntersectionObserver( (エントリ) => { if (!entries[0].isIntersecting) { 可視(真); } それ以外 { 可視(偽); } }、 { rootMargin: `-${header.height}px` }, ); オブザーバー.current.observe(ref); return () => オブザーバー.current?.disconnect(); }, [参照]);
ウィンドウにオブジェクトを追加するのはよくある間違いです。シナリオによっては、特にウィンドウ実行コンテキストで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 コンパイラーに通知します。必要な場合は引き続き Windows を使用できます。ただし、明示的な方法で使用する必要があります。
厳密モードが前の例に与える影響:
addElement
関数の場合、グローバル スコープから呼び出された場合、 this
未定義です
変数にconst | let | var
を指定しない場合、次のエラーが発生します。
キャッチされない参照エラー: b が定義されていません
DOM ノードもメモリ リークの影響を受けないわけではありません。それらへの参照を保存しないように注意する必要があります。そうしないと、アクセス可能な状態のままになるため、ガベージ コレクターはそれらをクリーンアップできなくなります。
小さなコードでそれを示します。
const 要素 = []; const list = document.getElementById('list'); 関数 addElement() { // ノードをクリーンアップします list.innerHTML = ''; const pElement= document.createElement('p'); const element = document.createTextNode(`要素 ${elements.length}` を追加します); pElement.appendChild(要素); list.appendChild(pElement); 要素.push(pElement); } document.getElementById('addElement').onclick = addElement;
addElement
関数はリストp
クリアし、新しい要素を子要素としてリストに追加することに注意してください。この新しく作成された要素はelements
配列に追加されます。
次回addElement
が実行されると、要素はリストp
から削除されますが、 elements
配列に格納されるため、ガベージ コレクションには適していません。
関数を数回実行した後、関数を監視します。
上のスクリーンショットで、ノードがどのように侵害されたかを確認してください。では、この問題をどうやって解決すればいいのでしょうか? elements
配列をクリアすると、ガベージ コレクションの対象になります。
この記事では、メモリ リークの最も一般的な方法について説明しました。 JavaScript 自体がメモリ リークを発生しないことは明らかです。むしろ、開発者側の意図しないメモリ保持によって引き起こされます。コードがクリーンであり、自分自身の後のクリーンアップを忘れない限り、リークは発生しません。
JavaScript でのメモリとガベージ コレクションの仕組みを理解することが必須です。一部の開発者は、これは自動的に行われるため、この問題について心配する必要はないと誤解しています。