JavaScript這幾種內(nèi)存泄露寫法,你要小心了
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
今天我想和你聊聊,前端開發(fā)過程中內(nèi)存泄露的問題。相信你在工作當(dāng)中遇到過這樣的情況,比如,相同的代碼在開發(fā)環(huán)境運(yùn)行得好好的,到線上運(yùn)行一段時(shí)間后就出現(xiàn)頁(yè)面運(yùn)行卡頓,比較嚴(yán)重時(shí),無用的內(nèi)存會(huì)持續(xù)遞增,從而導(dǎo)致整個(gè)系統(tǒng)卡頓,甚至崩潰。那么,這大概率是發(fā)生了內(nèi)存泄漏。 那么,你可以思考幾個(gè)問題:
好,帶著這三個(gè)問題,我們來學(xué)習(xí)今天的內(nèi)容。 什么是內(nèi)存泄漏?首先什么是內(nèi)存泄漏呢?是放在內(nèi)存中的變量憑空消失了嗎?其實(shí)并不是,而是不再使用的內(nèi)存沒有得到釋放,導(dǎo)致內(nèi)存一直在增加。我打個(gè)簡(jiǎn)單的比方:你跟別人要東西,你不停地要,不停地要,當(dāng)你不需要這些東西的時(shí)候你也不還給別人,這就是內(nèi)存泄漏。 那為什么會(huì)出現(xiàn)內(nèi)存泄漏呢,如果你了解內(nèi)存的生命周期,一定會(huì)知道是哪個(gè)環(huán)節(jié)導(dǎo)致了內(nèi)存泄漏。不管什么程序語(yǔ)言,它的生命周期一般可以按順序分為三個(gè)部分: 我們先來簡(jiǎn)單了解下這三個(gè)部分都做了什么事情:第一部分是內(nèi)存分配,當(dāng)我們創(chuàng)建一個(gè)函數(shù)或者變量時(shí),必須為它分配一定數(shù)量的內(nèi)存;第二部分是內(nèi)存的使用,對(duì)分配的內(nèi)存進(jìn)行讀和寫操作;第三部分是內(nèi)存釋放階段,將不需要的內(nèi)存進(jìn)行釋放。 所有語(yǔ)言的第二部分都是明確的。第一和第三部分在 C 或者 C++ 這類底層語(yǔ)言中是明確的,但在像 JavaScript 這些高級(jí)語(yǔ)言中,大部分都是隱含的。JavaScript 是在創(chuàng)建變量時(shí)自動(dòng)進(jìn)行了分配內(nèi)存,并且在不使用它們時(shí)“自動(dòng)”釋放,釋放的過程稱為垃圾回收。 問題就出現(xiàn)在這個(gè)“自動(dòng)”的垃圾回收,讓前端開發(fā)者錯(cuò)誤地以為他們可以不關(guān)心內(nèi)存管理。但是實(shí)際上瀏覽器 V8 引擎的垃圾回收只能解決一般問題,它自身有一些局限性。 如果你了解標(biāo)記 - 清除垃圾回收算法,就會(huì)知道,它是首先從根開始將可能被引用的對(duì)象用遞歸的方式進(jìn)行標(biāo)記,然后將沒有標(biāo)記到的對(duì)象作為垃圾進(jìn)行回收。 這個(gè)算法假定設(shè)置一個(gè)叫做根的對(duì)象,在 JavaScript 里,根就是是全局對(duì)象 window。垃圾回收器將定期從全局對(duì)象 window 開始,找所有從 window 開始引用的對(duì)象,然后找這些對(duì)象引用的對(duì)象……遞歸完成后,垃圾回收器將找到所有“可獲得”的對(duì)象和收集所有“不能獲得”的對(duì)象,最后將不可獲得的對(duì)象進(jìn)行回收。 但是垃圾回收機(jī)制會(huì)有這樣一個(gè)問題:假如有一些對(duì)象我們已經(jīng)不需要使用了,但是仍然能被訪問到,我們沒有對(duì)它進(jìn)行手動(dòng)清除,那么瀏覽器引擎的就不會(huì)對(duì)這個(gè)對(duì)象回收,當(dāng)無用的對(duì)象越來越多,就會(huì)導(dǎo)致內(nèi)存泄漏。那么哪些寫法會(huì)導(dǎo)致內(nèi)存泄漏呢?我總結(jié)了 JavaScript 中四類常見的內(nèi)存泄漏寫法和避免方法,給你參考。 哪些寫法會(huì)導(dǎo)致內(nèi)存泄漏?1.未聲明/意外的全局變量第一種,未聲明或者意外的全局變量。全局變量的生命周期最長(zhǎng),直到頁(yè)面關(guān)閉前,它都存活著,所以全局變量上的內(nèi)存一直都不會(huì)被回收。所以當(dāng)我們寫代碼的時(shí)候如果全局變量使用不當(dāng),沒有及時(shí)回收,或者拼寫錯(cuò)誤將某個(gè)變量掛載到全局變量時(shí),就會(huì)發(fā)生內(nèi)存泄漏了。
這段代碼中,test 函數(shù)中定義了一個(gè)變量 b,沒有使用 var 或者 let 變量進(jìn)行聲明,這時(shí) b 會(huì)成為全局變量,test 執(zhí)行后變量 b 不會(huì)被回收。解決方式也比較簡(jiǎn)單,在 JavaScript 文件中添加'use strict',開啟嚴(yán)格模式,這個(gè)時(shí)候就不能使用 b 這個(gè)意外的全局變量了,開發(fā)時(shí)就會(huì)在瀏覽器控制臺(tái)報(bào)錯(cuò),避免這種情況發(fā)生。
2.遺忘的定時(shí)器第二類常見的內(nèi)存泄漏是定時(shí)器 setTimeout 和 setInterval,它的生命周期是由瀏覽器專門的線程來維護(hù)的,所以當(dāng)在某個(gè)頁(yè)面使用了定時(shí)器,并且這個(gè)頁(yè)面銷毀時(shí),如果你沒有手動(dòng)去釋放清理這些定時(shí)器,那么這些定時(shí)器還是存活著的。 來看一個(gè)示例,這段代碼通過定時(shí)器注冊(cè)了一個(gè)回調(diào)函數(shù),該回調(diào)函數(shù)內(nèi)又持有當(dāng)前頁(yè)面 ID 為 Node 的 DOM 元素。當(dāng)頁(yè)面銷毀之后,由于定時(shí)器持有該頁(yè)面部分引用而造成定時(shí)器沒有被回收,進(jìn)而導(dǎo)致定時(shí)器內(nèi)部的數(shù)據(jù) someData 也無法被回收,就導(dǎo)致了內(nèi)存泄漏。
解決辦法是,在不使用定時(shí)器的時(shí)候?qū)⒍〞r(shí)器取消,setInterval 設(shè)置一個(gè) ID,然后就可以通過 clearInterval(id) 進(jìn)行取消了。
3.事件綁定第三種由“事件綁定”導(dǎo)致的內(nèi)存泄漏也非常常見,一般是由于事件響應(yīng)函數(shù)沒有及時(shí)移除,導(dǎo)致重復(fù)綁定或者 DOM 元素已經(jīng)移除后未處理事件響應(yīng)函數(shù)造成的,例如這段 React 代碼:
組件在掛載的時(shí)候監(jiān)聽了 resize 事件,但是在組件移除的時(shí)候沒有處理相應(yīng)函數(shù),假如 的掛載和移除非常頻繁,那么就會(huì)在 window 上綁定很多無用的事件監(jiān)聽函數(shù),最終導(dǎo)致內(nèi)存泄漏。 那怎么解決呢?我們可以通過在組件卸載 componentWillUmout 的時(shí)候移除監(jiān)聽事件來避免這個(gè)問題:
4.閉包最后一類比較重要,也是我們?cè)陂_發(fā)過程中經(jīng)常使用到的,那就是閉包。這里先聲明:閉包本身沒有錯(cuò),不會(huì)引起內(nèi)存泄漏,而是我們使用錯(cuò)誤導(dǎo)致的。 我們都知道,閉包有兩個(gè)主要作用,一是延伸變量作用域范圍,讀取函數(shù)內(nèi)部的變量。二是讓這些變量的值始終保持在內(nèi)存中。簡(jiǎn)單理解就是,一個(gè)作用域可以訪問另外一個(gè)函數(shù)內(nèi)部的局部變量。 那么,為什么會(huì)說閉包可能會(huì)導(dǎo)致內(nèi)存泄漏呢?函數(shù)本身會(huì)持有它定義時(shí)所在的詞法環(huán)境的引用,但通常情況下,使用完函數(shù)后,該函數(shù)所申請(qǐng)的內(nèi)存都會(huì)被回收了。但當(dāng)函數(shù)內(nèi)再返回一個(gè)函數(shù)時(shí),由于返回的函數(shù)持有外部函數(shù)的詞法環(huán)境,而返回的函數(shù)又被其他生命周期東西所持有,導(dǎo)致外部函數(shù)雖然執(zhí)行完了,但內(nèi)存卻無法被回收。 我們舉個(gè)例子來看一下閉包,這個(gè)例子中函數(shù) f1 里面返回了一個(gè)函數(shù) f2,f2 中使用了 f1 函數(shù)中的變量 n,這樣就形成了閉包。你可以想一下 console.log 的內(nèi)容應(yīng)該是什么?好,答案是在執(zhí)行完 f1 函數(shù)后,第一次調(diào)用 result 結(jié)果返回 1000,第二次調(diào)用結(jié)果返回了 1001。這說明了什么呢?說明變量 n 在函數(shù)執(zhí)行完并沒有被銷毀,而是繼續(xù)留在了內(nèi)存中。
正常來說,閉包并不是內(nèi)存泄漏,因?yàn)檫@種持有外部函數(shù)詞法環(huán)境本就是閉包的特性,就是為了讓這塊內(nèi)存不被回收,因?yàn)榭赡茉谖磥磉€需要用到,但這無疑會(huì)造成內(nèi)存的消耗,所以,我們不應(yīng)該濫用閉包。 總結(jié)今天的分享到這里就結(jié)束了,最后我們來回顧一下今天講的內(nèi)容。內(nèi)存泄漏發(fā)生在 JavaScript 內(nèi)存自動(dòng)回收階段,瀏覽器引擎在“自動(dòng)”回收階段使用的是標(biāo)記清除算法,可以將“不可獲得”的對(duì)象進(jìn)行回收,如果我們編寫代碼的時(shí)候?qū)σ恍┤肿兞刻幚聿划?dāng),定時(shí)器和事件綁定沒有及時(shí)清除,或者閉包使用不當(dāng),就會(huì)引起內(nèi)存泄漏問題。所以為了避免內(nèi)存泄漏,最重要對(duì)一點(diǎn)就是養(yǎng)成良好對(duì)編程習(xí)慣,比如內(nèi)存分配后,一定要注意寫好內(nèi)存釋放的代碼,有借有還,才能高效運(yùn)轉(zhuǎn),再借不難。 ?轉(zhuǎn)自https://juejin.cn/post/7490856819004620836 該文章在 2025/4/14 9:56:00 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |