五個(gè)常見(jiàn)的JavaScript內(nèi)存錯(cuò)誤
JavaScript沒(méi)有提供任何內(nèi)存管理原語(yǔ)。相反,內(nèi)存由JavaScript VM通過(guò)內(nèi)存回收過(guò)程管理。該過(guò)程稱(chēng)為垃圾收集。
由于我們不能強(qiáng)迫它運(yùn)行,我們?nèi)绾沃浪鼤?huì)正常工作?我們對(duì)此了解了什么?
- 腳本執(zhí)行在此過(guò)程中暫停。
- 它釋放內(nèi)存以實(shí)現(xiàn)無(wú)法訪問(wèn)的資源。
- 這是非確定性的。
- 它不會(huì)一次性檢查整個(gè)內(nèi)存,但將在多個(gè)周期中運(yùn)行。
- 這是不可預(yù)測(cè)的。它將在必要時(shí)執(zhí)行。
這是否意味著我們不必?fù)?dān)心資源和內(nèi)存分配?當(dāng)然不是。如果您不小心,您可能會(huì)創(chuàng)建一些內(nèi)存泄漏。
什么是內(nèi)存泄漏?
內(nèi)存泄漏是軟件無(wú)法回收的分配的存儲(chǔ)器。
javascript為您提供垃圾收集過(guò)程并不意味著您可以從內(nèi)存泄漏中安全。為了有資格獲得垃圾收集,必須在其他地方引用對(duì)象。如果您持有對(duì)未使用的資源的引用,則會(huì)阻止這些資源未分配。這被稱(chēng)為無(wú)意的記憶保留。
泄漏內(nèi)存可能導(dǎo)致更頻繁的垃圾收集器運(yùn)行。由于此過(guò)程將阻止腳本運(yùn)行,因此可能會(huì)減慢您的Web應(yīng)用程序。這將使您的表現(xiàn)較少,這將由用戶注意到。它甚至可以導(dǎo)致您的Web應(yīng)用程序崩潰。
我們?nèi)绾畏乐刮覀兊腤eb應(yīng)用程序泄漏內(nèi)存?這很簡(jiǎn)單:通過(guò)避免保留不必要的資源。讓我們看看可能發(fā)生的最常見(jiàn)的場(chǎng)景。
計(jì)時(shí)器監(jiān)聽(tīng)器
讓我們來(lái)看看SetInterval定時(shí)器。它是一個(gè)常用的Web API功能。
“窗口和工作接口提供的setInterval()方法,重復(fù)調(diào)用函數(shù)或執(zhí)行代碼片段,每個(gè)呼叫之間的固定時(shí)間延遲。它返回唯一標(biāo)識(shí)間隔的間隔ID,因此您可以通過(guò)調(diào)用ClearInterval()稍后刪除它。該方法由WindoworWorkerglobalscope Mixin定義。“
- MDN Web Docs |
讓我們創(chuàng)建一個(gè)調(diào)用回調(diào)函數(shù)的組件,以發(fā)出x循環(huán)后的完成。我正在為這個(gè)特定的例子做出反應(yīng),但這適用于任何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;
起初,看起來(lái)沒(méi)有什么是錯(cuò)的。讓我們創(chuàng)建一個(gè)觸發(fā)此計(jì)時(shí)器的組件,并分析其內(nèi)存性能:
- 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獲得內(nèi)存使用的結(jié)果:
您可以看到在擊中重試按鈕時(shí)分配了越來(lái)越多的內(nèi)存。這意味著分配的先前內(nèi)存并沒(méi)有釋放。間隔計(jì)時(shí)器仍在運(yùn)行而不是被替換。
我們?nèi)绾谓鉀Q這個(gè)問(wèn)題?setInterval的返回是我們可以使用的間隔ID來(lái)取消間隔。在這個(gè)特定的方案中,我們可以在組件上卸載一旦組件才能調(diào)用ClearInterval。
- useEffect(() => {
- const intervalId = setInterval(() => {
- if (currentCicles.current >= cicles) {
- onFinish();
- return;
- }
- currentCicles.current++;
- }, 500);
- return () => clearInterval(intervalId);
- }, [])
有時(shí),在代碼審查中發(fā)現(xiàn)這些問(wèn)題很難。最好的做法是創(chuàng)建抽象,您可以管理所有復(fù)雜性。
正如我們?cè)诖耸褂玫姆磻?yīng),我們可以在自定義掛鉤中包裝所有這些邏輯:
- 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;
現(xiàn)在,無(wú)論何時(shí)需要使用SetInterval,您都可以執(zhí)行以下操作:
- const handleTimeout = () => ...;
- useTimeout(100, handleTimeout);
現(xiàn)在,您可以使用此USETIMEOUT掛鉤而無(wú)需擔(dān)心內(nèi)存泄露,它都是由抽象管理的。
2. 事件監(jiān)聽(tīng)器
Web API提供了大量的事件偵聽(tīng)器,您可以自己掛鉤。以前,我們覆蓋了settimout?,F(xiàn)在我們將看addeventlistener。
讓我們?yōu)槲覀兊腤eb應(yīng)用程序創(chuàng)建一個(gè)鍵盤(pán)快捷功能。由于我們?cè)诓煌?yè)面上有不同的功能,因此我們將創(chuàng)建不同的快捷函數(shù):
- 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);
一切似乎很好,除了我們?cè)趫?zhí)行第二個(gè)AddeventListener時(shí)沒(méi)有清潔先前的鍵。此代碼而不是更換我們的keyup偵聽(tīng)器,而不是更換keyup偵聽(tīng)器。這意味著當(dāng)按下鍵時(shí),它將觸發(fā)兩個(gè)功能。
要清除以前的回調(diào),我們需要使用remove eventListener。讓我們看看代碼示例:
- document.removeEventListener(‘keyup’, homeShortcuts);
讓我們重構(gòu)代碼以防止這種不需要的行為:
- 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);
作為拇指的規(guī)則,當(dāng)使用來(lái)自全局對(duì)象的工具時(shí),您需要謹(jǐn)慎且負(fù)責(zé)任。
3. 觀察者
觀察者是大量開(kāi)發(fā)人員未知的瀏覽器Web API功能。如果您想檢查HTML元素的可見(jiàn)性或大小的更改,它們是強(qiáng)大的。
讓我們檢查交叉點(diǎn)觀察者API:
“Intersection Observer API提供了一種異步地觀察目標(biāo)元素與祖先元素或頂級(jí)文檔的視口的交叉點(diǎn)的變化。”
- MDN Web Docs |
盡可能強(qiáng)大,您需要負(fù)責(zé)任地使用它。完成觀察對(duì)象后,您需要取消監(jiān)視過(guò)程。
讓我們看一些代碼:
- 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]);
上面的代碼看起來(lái)很好。但是,一旦組件未安裝,觀察者會(huì)發(fā)生什么?它不會(huì)被清除,所以你會(huì)泄漏內(nèi)存。我們?cè)鯓硬拍芙鉀Q這個(gè)問(wèn)題?只需使用斷開(kāi)連接方法:
現(xiàn)在我們可以確定,當(dāng)組件卸載時(shí),我們的觀察者將被斷開(kāi)連接。
4. 窗口對(duì)象
將對(duì)象添加到窗口是一個(gè)常見(jiàn)的錯(cuò)誤。在某些情況下,可能很難找到 - 特別是如果您使用窗口執(zhí)行上下文中的此關(guān)鍵字。
讓我們來(lái)看看以下例子:
- function addElement(element) {
- if (!this.stack) {
- this.stack = {
- elements: []
- }
- }
- this.stack.elements.push(element);
- }
它看起來(lái)無(wú)害,但這取決于你調(diào)用一個(gè)addelement的上下文。如果從窗口上下文中調(diào)用AddElement,則會(huì)開(kāi)始查看堆積的項(xiàng)目。
另一個(gè)問(wèn)題可能是錯(cuò)誤地定義全局變量:
- var a = 'example 1'; // scoped to the place where var was createdb = 'example 2'; // added to the Window object
為防止這種問(wèn)題,始終以嚴(yán)格模式執(zhí)行JavaScript:
- "use strict"
通過(guò)使用嚴(yán)格模式,您將暗示您想要保護(hù)自己免受這些類(lèi)型的行為保護(hù)的JavaScript編譯器。當(dāng)您需要時(shí),您仍然可以使用窗口。但是,您必須以明確的方式使用它。
如何影響我們之前的示例的嚴(yán)格模式:
- 在Addelement函數(shù)上,從全局范圍內(nèi)調(diào)用時(shí),這將是未定義的。
- 如果您未指定const |左撇子var在變量上,您將收到以下錯(cuò)誤:
- Uncaught ReferenceError: b is not defined
5. 持有DOM參考
DOM節(jié)點(diǎn)也沒(méi)有內(nèi)存泄漏。你需要小心不要抓住他們的參考。否則,垃圾收集器將無(wú)法清除它們,因?yàn)樗鼈內(nèi)匀豢梢缘竭_(dá)。
讓我們看一個(gè)小的代碼示例來(lái)說(shuō)明這個(gè):
- 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;
請(qǐng)注意,AddElement函數(shù)清除列表DIV并將新元素添加為子項(xiàng)。此新創(chuàng)建的元素將添加到元素?cái)?shù)組中。
下次執(zhí)行AddElement,將從列表Div中刪除該元素。但是,它不會(huì)有資格獲得垃圾收集,因?yàn)樗鎯?chǔ)在元素?cái)?shù)組中。這使得它可以到達(dá)。這將使您在每個(gè)addelement執(zhí)行上的節(jié)點(diǎn)。
讓我們?cè)趲讉€(gè)執(zhí)行之后監(jiān)視函數(shù):
我們可以在上面的屏幕截圖中看到節(jié)點(diǎn)如何泄露。我們?cè)鯓硬拍芙鉀Q這個(gè)問(wèn)題?清除元素?cái)?shù)組將使它們有資格獲得垃圾收集。
結(jié)論
在本文中,我們已經(jīng)看到了最常見(jiàn)的方法可以泄露。很明顯,JavaScript不會(huì)泄漏內(nèi)存本身。相反,它是由從開(kāi)發(fā)人員側(cè)的無(wú)意的記憶保留引起的。只要代碼整潔,我們就不會(huì)忘記在自己之后清理,不會(huì)發(fā)生泄漏。
了解JavaScript中的內(nèi)存和垃圾收集工作是必須的。一些開(kāi)發(fā)人員獲得虛假印象,因?yàn)樗亲詣?dòng)的,他們不需要擔(dān)心它。
建議在Web應(yīng)用程序上定期運(yùn)行瀏覽器分析器工具。這是唯一能夠肯定沒(méi)有泄漏并留下的方法。Chrome開(kāi)發(fā)人員性能選項(xiàng)卡是開(kāi)始檢測(cè)某些異常的地點(diǎn)。瀏覽問(wèn)題后,您可以通過(guò)拍攝快照并進(jìn)行比較,使用Profiler選項(xiàng)卡深入挖掘它。
有時(shí),我們花費(fèi)時(shí)間優(yōu)化方法,忘記內(nèi)存在我們的Web應(yīng)用程序的性能中播放了一個(gè)很大的部分。
干杯!
原文鏈接:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2