五個常見的JavaScript內存錯誤
JavaScript沒有提供任何內存管理原語。相反,內存由JavaScript VM通過內存回收過程管理。該過程稱為垃圾收集。
由于我們不能強迫它運行,我們如何知道它會正常工作?我們對此了解了什么?
- 腳本執行在此過程中暫停。
- 它釋放內存以實現無法訪問的資源。
- 這是非確定性的。
- 它不會一次性檢查整個內存,但將在多個周期中運行。
- 這是不可預測的。它將在必要時執行。
這是否意味著我們不必擔心資源和內存分配?當然不是。如果您不小心,您可能會創建一些內存泄漏。
什么是內存泄漏?
內存泄漏是軟件無法回收的分配的存儲器。
javascript為您提供垃圾收集過程并不意味著您可以從內存泄漏中安全。為了有資格獲得垃圾收集,必須在其他地方引用對象。如果您持有對未使用的資源的引用,則會阻止這些資源未分配。這被稱為無意的記憶保留。
泄漏內存可能導致更頻繁的垃圾收集器運行。由于此過程將阻止腳本運行,因此可能會減慢您的Web應用程序。這將使您的表現較少,這將由用戶注意到。它甚至可以導致您的Web應用程序崩潰。
我們如何防止我們的Web應用程序泄漏內存?這很簡單:通過避免保留不必要的資源。讓我們看看可能發生的最常見的場景。
計時器監聽器
讓我們來看看SetInterval定時器。它是一個常用的Web API功能。
“窗口和工作接口提供的setInterval()方法,重復調用函數或執行代碼片段,每個呼叫之間的固定時間延遲。它返回唯一標識間隔的間隔ID,因此您可以通過調用ClearInterval()稍后刪除它。該方法由WindoworWorkerglobalscope Mixin定義。“
- MDN Web Docs |
讓我們創建一個調用回調函數的組件,以發出x循環后的完成。我正在為這個特定的例子做出反應,但這適用于任何FE框架。
- import React, { useRef } from 'react';
- const Timer = ({ cicles, onFinish }) => {
- const currentCicles = useRef(0);
- setInterval(() => {
- if (currentCicles.current >= cicles) {
- onFinish();
- return;
- }
- currentCicles.current++;
- }, 500);
- return (
- <div>Loading ...</div>
- );
- }
- export default Timer;
起初,看起來沒有什么是錯的。讓我們創建一個觸發此計時器的組件,并分析其內存性能:
- import React, { useState } from 'react';
- import styles from '../styles/Home.module.css'
- import Timer from '../components/Timer';
- export default function Home() {
- const [showTimer, setShowTimer] = useState();
- const onFinish = () => setShowTimer(false);
- return (
- <div className={styles.container}>
- {showTimer ? (
- <Timer cicles={10} onFinish={onFinish} />
- ): (
- <button onClick={() => setShowTimer(true)}>
- Retry
- </button>
- )}
- </div>
- )
- }
在重試按鈕上單擊幾次后,這是我們使用Chrome Dev Tools獲得內存使用的結果:
您可以看到在擊中重試按鈕時分配了越來越多的內存。這意味著分配的先前內存并沒有釋放。間隔計時器仍在運行而不是被替換。
我們如何解決這個問題?setInterval的返回是我們可以使用的間隔ID來取消間隔。在這個特定的方案中,我們可以在組件上卸載一旦組件才能調用ClearInterval。
- useEffect(() => {
- const intervalId = setInterval(() => {
- if (currentCicles.current >= cicles) {
- onFinish();
- return;
- }
- currentCicles.current++;
- }, 500);
- return () => clearInterval(intervalId);
- }, [])
有時,在代碼審查中發現這些問題很難。最好的做法是創建抽象,您可以管理所有復雜性。
正如我們在此使用的反應,我們可以在自定義掛鉤中包裝所有這些邏輯:
- import { useEffect } from 'react';
- export const useTimeout = (refreshCycle = 100, callback) => {
- useEffect(() => {
- if (refreshCycle <= 0) {
- setTimeout(callback, 0);
- return;
- }
- const intervalId = setInterval(() => {
- callback();
- }, refreshCycle);
- return () => clearInterval(intervalId);
- }, [refreshCycle, setInterval, clearInterval]);
- };
- export default useTimeout;
現在,無論何時需要使用SetInterval,您都可以執行以下操作:
- const handleTimeout = () => ...;
- useTimeout(100, handleTimeout);
現在,您可以使用此USETIMEOUT掛鉤而無需擔心內存泄露,它都是由抽象管理的。
2. 事件監聽器
Web API提供了大量的事件偵聽器,您可以自己掛鉤。以前,我們覆蓋了settimout?,F在我們將看addeventlistener。
讓我們為我們的Web應用程序創建一個鍵盤快捷功能。由于我們在不同頁面上有不同的功能,因此我們將創建不同的快捷函數:
- function homeShortcuts({ key}) {
- if (key === 'E') {
- console.log('edit widget')
- }
- }
- // user lands on home and we execute
- document.addEventListener('keyup', homeShortcuts);
- // user does some stuff and navigates to settings
- function settingsShortcuts({ key}) {
- if (key === 'E') {
- console.log('edit setting')
- }
- }
- // user lands on home and we execute
- document.addEventListener('keyup', settingsShortcuts);
一切似乎很好,除了我們在執行第二個AddeventListener時沒有清潔先前的鍵。此代碼而不是更換我們的keyup偵聽器,而不是更換keyup偵聽器。這意味著當按下鍵時,它將觸發兩個功能。
要清除以前的回調,我們需要使用remove eventListener。讓我們看看代碼示例:
- document.removeEventListener(‘keyup’, homeShortcuts);
讓我們重構代碼以防止這種不需要的行為:
- function homeShortcuts({ key}) {
- if (key === 'E') {
- console.log('edit widget')
- }
- }
- // user lands on home and we execute
- document.addEventListener('keyup', homeShortcuts);
- // user does some stuff and navigates to settings
- function settingsShortcuts({ key}) {
- if (key === 'E') {
- console.log('edit setting')
- }
- }
- // user lands on home and we execute
- document.removeEventListener('keyup', homeShortcuts);
- document.addEventListener('keyup', settingsShortcuts);
作為拇指的規則,當使用來自全局對象的工具時,您需要謹慎且負責任。
3. 觀察者
觀察者是大量開發人員未知的瀏覽器Web API功能。如果您想檢查HTML元素的可見性或大小的更改,它們是強大的。
讓我們檢查交叉點觀察者API:
“Intersection Observer API提供了一種異步地觀察目標元素與祖先元素或頂級文檔的視口的交叉點的變化。”
- MDN Web Docs |
盡可能強大,您需要負責任地使用它。完成觀察對象后,您需要取消監視過程。
讓我們看一些代碼:
- const ref = ...
- const visible = (visible) => {
- console.log(`It is ${visible}`);
- }
- useEffect(() => {
- if (!ref) {
- return;
- }
- observer.current = new IntersectionObserver(
- (entries) => {
- if (!entries[0].isIntersecting) {
- visible(true);
- } else {
- visbile(false);
- }
- },
- { rootMargin: `-${header.height}px` },
- );
- observer.current.observe(ref);
- }, [ref]);
上面的代碼看起來很好。但是,一旦組件未安裝,觀察者會發生什么?它不會被清除,所以你會泄漏內存。我們怎樣才能解決這個問題?只需使用斷開連接方法:
現在我們可以確定,當組件卸載時,我們的觀察者將被斷開連接。
4. 窗口對象
將對象添加到窗口是一個常見的錯誤。在某些情況下,可能很難找到 - 特別是如果您使用窗口執行上下文中的此關鍵字。
讓我們來看看以下例子:
- function addElement(element) {
- if (!this.stack) {
- this.stack = {
- elements: []
- }
- }
- this.stack.elements.push(element);
- }
它看起來無害,但這取決于你調用一個addelement的上下文。如果從窗口上下文中調用AddElement,則會開始查看堆積的項目。
另一個問題可能是錯誤地定義全局變量:
- var a = 'example 1'; // scoped to the place where var was createdb = 'example 2'; // added to the Window object
為防止這種問題,始終以嚴格模式執行JavaScript:
- "use strict"
通過使用嚴格模式,您將暗示您想要保護自己免受這些類型的行為保護的JavaScript編譯器。當您需要時,您仍然可以使用窗口。但是,您必須以明確的方式使用它。
如何影響我們之前的示例的嚴格模式:
- 在Addelement函數上,從全局范圍內調用時,這將是未定義的。
- 如果您未指定const |左撇子var在變量上,您將收到以下錯誤:
- Uncaught ReferenceError: b is not defined
5. 持有DOM參考
DOM節點也沒有內存泄漏。你需要小心不要抓住他們的參考。否則,垃圾收集器將無法清除它們,因為它們仍然可以到達。
讓我們看一個小的代碼示例來說明這個:
- const elements = [];
- const list = document.getElementById('list');
- function addElement() {
- // clean nodes
- list.innerHTML = '';
- const divElement= document.createElement('div');
- const element = document.createTextNode(`adding element ${elements.length}`);
- divElement.appendChild(element);
- list.appendChild(divElement);
- elements.push(divElement);
- }
- document.getElementById('addElement').onclick = addElement;
請注意,AddElement函數清除列表DIV并將新元素添加為子項。此新創建的元素將添加到元素數組中。
下次執行AddElement,將從列表Div中刪除該元素。但是,它不會有資格獲得垃圾收集,因為它存儲在元素數組中。這使得它可以到達。這將使您在每個addelement執行上的節點。
讓我們在幾個執行之后監視函數:
我們可以在上面的屏幕截圖中看到節點如何泄露。我們怎樣才能解決這個問題?清除元素數組將使它們有資格獲得垃圾收集。
結論
在本文中,我們已經看到了最常見的方法可以泄露。很明顯,JavaScript不會泄漏內存本身。相反,它是由從開發人員側的無意的記憶保留引起的。只要代碼整潔,我們就不會忘記在自己之后清理,不會發生泄漏。
了解JavaScript中的內存和垃圾收集工作是必須的。一些開發人員獲得虛假印象,因為它是自動的,他們不需要擔心它。
建議在Web應用程序上定期運行瀏覽器分析器工具。這是唯一能夠肯定沒有泄漏并留下的方法。Chrome開發人員性能選項卡是開始檢測某些異常的地點。瀏覽問題后,您可以通過拍攝快照并進行比較,使用Profiler選項卡深入挖掘它。
有時,我們花費時間優化方法,忘記內存在我們的Web應用程序的性能中播放了一個很大的部分。
干杯!
原文鏈接:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2