<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          5個常見的JavaScript內(nèi)存錯誤

          共 12205字,需瀏覽 25分鐘

           ·

          2022-01-10 23:44

          作者:前端小智

          簡介:掘金、思否百萬閱讀,勵志退休后,回家擺地攤的人。

          來源:SegmentFault  思否社區(qū)


          JavaScript 不提供任何內(nèi)存管理操作。相反,內(nèi)存由 JavaScript VM 通過內(nèi)存回收過程管理,該過程稱為垃圾收集


          既然我們不能強制的垃圾回收,那我們怎么知道它能正常工作?我們對它又了解多少呢?


          • 腳本執(zhí)行在此過程中暫停
          • 它為不可訪問的資源釋放內(nèi)存
          • 它是不確定的
          • 它不會一次檢查整個內(nèi)存,而是在多個周期中運行
          • 它是不可預(yù)測的,但它會在必要時執(zhí)行

          這是否意味著無需擔(dān)心資源和內(nèi)存分配問題?當(dāng)然不是。如果我們一不小心,可能會產(chǎn)生一些內(nèi)存泄漏。

          什么是內(nèi)存泄漏?



          內(nèi)存泄漏是軟件無法回收的已分配的內(nèi)存塊。

          Javascript 提供了一個垃圾收集程序,但這并不意味著我們就能避免內(nèi)存泄漏。為了符合垃圾收集的條件,該對象必須不被其他地方引用。如果持有對未使用的資源的引用,這將會阻止這些資源被回收。這就是所謂的無意識的內(nèi)存保持

          泄露內(nèi)存可能會導(dǎo)致垃圾收集器更頻繁地運行。由于這個過程會阻止腳本的運行,它可能會讓我們程序卡起來,這么一卡,挑剔的用戶肯定會注意到,一用不爽了,那這個產(chǎn)品離下線的日子就不完了。更嚴(yán)重可能會讓整個應(yīng)用奔潰,那就gg了。

          如何防止內(nèi)存泄漏? 主要還是我們應(yīng)該避免保留不必要的資源。來看看一些常見的場景。

          1.計時器的監(jiān)聽


          setInterval() 方法重復(fù)調(diào)用函數(shù)或執(zhí)行代碼片段,每次調(diào)用之間有固定的時間延遲。它返回一個時間間隔ID,該ID唯一地標(biāo)識時間間隔,因此您可以稍后通過調(diào)用 clearInterval() 來刪除它。

          我們創(chuàng)建一個組件,它調(diào)用一個回調(diào)函數(shù)來表示它在x個循環(huán)之后完成了。我在這個例子中使用React,但這適用于任何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;

          一看,好像沒啥問題。不急,我們再創(chuàng)建一個觸發(fā)這個定時器的組件,并分析其內(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>
              )
          }

          Retry 按鈕上單擊幾次后,這是使用Chrome Dev Tools獲取內(nèi)存使用的結(jié)果:


          當(dāng)我們點擊重試按鈕時,可以看到分配的內(nèi)存越來越多。這說明之前分配的內(nèi)存沒有被釋放。計時器仍然在運行而不是被替換。

          怎么解決這個問題?setInterval 的返回值是一個間隔 ID,我們可以用它來取消這個間隔。在這種特殊情況下,我們可以在組件卸載后調(diào)用 clearInterval

          useEffect(() => {
              const intervalId = setInterval(() => {
                  if (currentCicles.current >= cicles) {
                      onFinish();
                      return;
                  }
                  currentCicles.current++;
              }, 500);

              return () => clearInterval(intervalId);
          }, [])

          有時,在編寫代碼時,很難發(fā)現(xiàn)這個問題,最好的方式,還是要把組件抽象化。
          這里使用的是React,我們可以把所有這些邏輯都包裝在一個自定義的 Hook 中。

          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)在需要使用setInterval時,都可以這樣做:

          const handleTimeout = () => ...;

          useTimeout(100, handleTimeout);

          現(xiàn)在你可以使用這個useTimeout Hook,而不必?fù)?dān)心內(nèi)存被泄露,這也是抽象化的好處。

          2.事件監(jiān)聽


          Web API提供了大量的事件監(jiān)聽器。在前面,我們討論了setTimeout。現(xiàn)在來看看 addEventListener

          在這個事例中,我們創(chuàng)建一個鍵盤快捷鍵功能。由于我們在不同的頁面上有不同的功能,所以將創(chuàng)建不同的快捷鍵功能。

          function homeShortcuts({ key}) {
              if (key === 'E') {
                  console.log('edit widget')
              }
          }

          // 用戶在主頁上登陸,我們執(zhí)行
          document.addEventListener('keyup', homeShortcuts); 


          // 用戶做一些事情,然后導(dǎo)航到設(shè)置

          function settingsShortcuts({ key}) {
              if (key === 'E') {
                  console.log('edit setting')
              }
          }

          // 用戶在主頁上登陸,我們執(zhí)行
          document.addEventListener('keyup', settingsShortcuts);

          看起來還是很好,除了在執(zhí)行第二個 addEventListener 時沒有清理之前的 keyup。這段代碼不是替換我們的 keyup 監(jiān)聽器,而是將添加另一個 callback。這意味著,當(dāng)一個鍵被按下時,它將觸發(fā)兩個函數(shù)。

          要清除之前的回調(diào),我們需要使用 removeEventListener :

          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);

          根據(jù)經(jīng)驗,當(dāng)使用來自全局對象的工具時,需要灰常小心。

          3.Observers


          Observers 是一個瀏覽器的 Web API功能,很多開發(fā)者都不知道。如果你想檢查HTML元素的可見性或大小的變化,這個就很強大了。

          IntersectionObserver接口 (從屬于Intersection Observer API) 提供了一種異步觀察目標(biāo)元素與其祖先元素或頂級文檔視窗(viewport)交叉狀態(tài)的方法。祖先元素與視窗(viewport)被稱為根(root)。

          盡管它很強大,但我們也要謹(jǐn)慎的使用它。一旦完成了對對象的觀察,就要記得在不用的時候取消它。

          看看代碼:

          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]);

          上面的代碼看起來不錯。然而,一旦組件被卸載,觀察者會發(fā)生什么?它不會被清除,那內(nèi)存可就泄漏了。我們怎么解決這個問題呢?只需要使用 disconnect 方法:

          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);

              return () => observer.current?.disconnect();
          }, [ref]);

          4. Window Object


          向 Window 添加對象是一個常見的錯誤。在某些場景中,可能很難找到它,特別是在使用 Window Execution上下文中的this關(guān)鍵字。看看下面的例子:

          function addElement(element) {
              if (!this.stack) {
                  this.stack = {
                      elements: []
                  }
              }

              this.stack.elements.push(element);
          }

          它看起來無害,但這取決于你從哪個上下文調(diào)用addElement。如果你從Window Context調(diào)用addElement,那就會越堆越多。

          另一個問題可能是錯誤地定義了一個全局變量:

          var a = 'example 1'; // 作用域限定在創(chuàng)建var的地方
          b = 'example 2'; // 添加到Window對象中

          要防止這種問題可以使用嚴(yán)格模式:

          "use strict"

          通過使用嚴(yán)格模式,向JavaScript編譯器暗示,你想保護自己免受這些行為的影響。當(dāng)你需要時,你仍然可以使用Window。不過,你必須以明確的方式使用它。

          嚴(yán)格模式是如何影響我們前面的例子:

          • 對于 addElement 函數(shù),當(dāng)從全局作用域調(diào)用時,this 是未定義的

          • 如果沒有在一個變量上指定const | let | var,你會得到以下錯誤:


          Uncaught ReferenceError: b is not defined

          5. 持有DOM引用


          DOM節(jié)點也不能避免內(nèi)存泄漏。我們需要注意不要保存它們的引用。否則,垃圾回收器將無法清理它們,因為它們?nèi)匀皇强稍L問的。

          用一小段代碼演示一下:

          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 函數(shù)清除列表 div,并將一個新元素作為子元素添加到它中。這個新創(chuàng)建的元素被添加到 elements 數(shù)組中。

          下一次執(zhí)行 addElement 時,該元素將從列表 div 中刪除,但是它不適合進行垃圾收集,因為它存儲在 elements 數(shù)組中。

          我們在執(zhí)行幾次之后監(jiān)視函數(shù):


          在上面的截圖中看到節(jié)點是如何被泄露的。那怎么解決這個問題?清除 elements 數(shù)組將使它們有資格進行垃圾收集。


          總結(jié)



          在這篇文章中,我們已經(jīng)看到了最常見的內(nèi)存泄露方式。很明顯,JavaScript本身并沒有泄漏內(nèi)存。相反,它是由開發(fā)者方面無意的內(nèi)存保持造成的。只要代碼是整潔的,而且我們不忘自己清理,就不會發(fā)生泄漏。

          了解內(nèi)存和垃圾回收在JavaScript中是如何工作的是必須的。一些開發(fā)者得到了錯誤的意識,認(rèn)為由于它是自動的,所以他們不需要擔(dān)心這個問題。



          點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 27
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  有没有免费的黄色网址 | 操比视频在线观看 | 超碰人人爱 | 国产搞黄色片网 | 91在线无码精品秘 蜜桃 |