js內(nèi)存泄漏場景、如何監(jiān)控及分析詳解
前言
Q:什么是內(nèi)存泄漏?
字面上的意思,申請的內(nèi)存沒有及時(shí)回收掉,被泄漏了
Q:為什么會(huì)發(fā)生內(nèi)存泄漏?
雖然前端有垃圾回收機(jī)制,但當(dāng)某塊無用的內(nèi)存,卻無法被垃圾回收機(jī)制認(rèn)為是垃圾時(shí),也就發(fā)生內(nèi)存泄漏了
而垃圾回收機(jī)制通常是使用標(biāo)志清除策略,簡單說,也就是引用從根節(jié)點(diǎn)開始是否可達(dá)來判定是否是垃圾
上面是發(fā)生內(nèi)存泄漏的根本原因,直接原因則是,當(dāng)不同生命周期的兩個(gè)東西相互通信時(shí),一方生命到期該回收了,卻被另一方還持有時(shí),也就發(fā)生內(nèi)存泄漏了
所以,下面就來講講,哪些場景會(huì)造成內(nèi)存泄漏
哪些情況會(huì)引起內(nèi)存泄漏
1. 意外的全局變量
全局變量的生命周期最長,直到頁面關(guān)閉前,它都存活著,所以全局變量上的內(nèi)存一直都不會(huì)被回收
當(dāng)全局變量使用不當(dāng),沒有及時(shí)回收(手動(dòng)賦值 null),或者拼寫錯(cuò)誤等將某個(gè)變量掛載到全局變量時(shí),也就發(fā)生內(nèi)存泄漏了
2. 遺忘的定時(shí)器
setTimeout 和 setInterval 是由瀏覽器專門線程來維護(hù)它的生命周期,所以當(dāng)在某個(gè)頁面使用了定時(shí)器,當(dāng)該頁面銷毀時(shí),沒有手動(dòng)去釋放清理這些定時(shí)器的話,那么這些定時(shí)器還是存活著的
也就是說,定時(shí)器的生命周期并不掛靠在頁面上,所以當(dāng)在當(dāng)前頁面的 js 里通過定時(shí)器注冊了某個(gè)回調(diào)函數(shù),而該回調(diào)函數(shù)內(nèi)又持有當(dāng)前頁面某個(gè)變量或某些 DOM 元素時(shí),就會(huì)導(dǎo)致即使頁面銷毀了,由于定時(shí)器持有該頁面部分引用而造成頁面無法正常被回收,從而導(dǎo)致內(nèi)存泄漏了
如果此時(shí)再次打開同個(gè)頁面,內(nèi)存中其實(shí)是有雙份頁面數(shù)據(jù)的,如果多次關(guān)閉、打開,那么內(nèi)存泄漏會(huì)越來越嚴(yán)重
而且這種場景很容易出現(xiàn),因?yàn)槭褂枚〞r(shí)器的人很容易遺忘清除
3. 使用不當(dāng)?shù)拈]包
函數(shù)本身會(huì)持有它定義時(shí)所在的詞法環(huán)境的引用,但通常情況下,使用完函數(shù)后,該函數(shù)所申請的內(nèi)存都會(huì)被回收了
但當(dāng)函數(shù)內(nèi)再返回一個(gè)函數(shù)時(shí),由于返回的函數(shù)持有外部函數(shù)的詞法環(huán)境,而返回的函數(shù)又被其他生命周期東西所持有,導(dǎo)致外部函數(shù)雖然執(zhí)行完了,但內(nèi)存卻無法被回收
所以,返回的函數(shù),它的生命周期應(yīng)盡量不宜過長,方便該閉包能夠及時(shí)被回收
正常來說,閉包并不是內(nèi)存泄漏,因?yàn)檫@種持有外部函數(shù)詞法環(huán)境本就是閉包的特性,就是為了讓這塊內(nèi)存不被回收,因?yàn)榭?/p>
能在未來還需要用到,但這無疑會(huì)造成內(nèi)存的消耗,所以,不宜爛用就是了
4. 遺漏的 DOM 元素
DOM 元素的生命周期正常是取決于是否掛載在 DOM 樹上,當(dāng)從 DOM 樹上移除時(shí),也就可以被銷毀回收了
但如果某個(gè) DOM 元素,在 js 中也持有它的引用時(shí),那么它的生命周期就由 js 和是否在 DOM 樹上兩者決定了,記得移除時(shí),兩個(gè)地方都需要去清理才能正?;厥账?/p>
5. 網(wǎng)絡(luò)回調(diào)
某些場景中,在某個(gè)頁面發(fā)起網(wǎng)絡(luò)請求,并注冊一個(gè)回調(diào),且回調(diào)函數(shù)內(nèi)持有該頁面某些內(nèi)容,那么,當(dāng)該頁面銷毀時(shí),應(yīng)該注銷網(wǎng)絡(luò)的回調(diào),否則,因?yàn)榫W(wǎng)絡(luò)持有頁面部分內(nèi)容,也會(huì)導(dǎo)致頁面部分內(nèi)容無法被回收
如何監(jiān)控內(nèi)存泄漏
內(nèi)存泄漏是可以分成兩類的,一種是比較嚴(yán)重的,泄漏的就一直回收不回來了,另一種嚴(yán)重程度稍微輕點(diǎn),就是沒有及時(shí)清理導(dǎo)致的內(nèi)存泄漏,一段時(shí)間后還是可以被清理掉
不管哪一種,利用開發(fā)者工具抓到的內(nèi)存圖,應(yīng)該都會(huì)看到一段時(shí)間內(nèi),內(nèi)存占用不斷的直線式下降,這是因?yàn)椴粩喟l(fā)生 GC,也就是垃圾回收導(dǎo)致的
針對第一種比較嚴(yán)重的,會(huì)發(fā)現(xiàn),內(nèi)存圖里即使不斷發(fā)生 GC 后,所使用的內(nèi)存總量仍舊在不斷增長
另外,內(nèi)存不足會(huì)造成不斷 GC,而 GC 時(shí)是會(huì)阻塞主線程的,所以會(huì)影響到頁面性能,造成卡頓,所以內(nèi)存泄漏問題還是需要關(guān)注的
我們假設(shè)這么一種場景,然后來用開發(fā)者工具查看下內(nèi)存泄漏:
場景一:在某個(gè)函數(shù)內(nèi)申請一塊內(nèi)存,然后該函數(shù)在短時(shí)間內(nèi)不斷被調(diào)用
// 點(diǎn)擊按鈕,就執(zhí)行一次函數(shù),申請一塊內(nèi)存 startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1); });
一個(gè)頁面能夠使用的內(nèi)存是有限的,當(dāng)內(nèi)存不足時(shí),就會(huì)觸發(fā)垃圾回收機(jī)制去回收沒用的內(nèi)存
而在函數(shù)內(nèi)部使用的變量都是局部變量,函數(shù)執(zhí)行完畢,這塊內(nèi)存就沒用可以被回收了
所以當(dāng)我們短時(shí)間內(nèi)不斷調(diào)用該函數(shù)時(shí),可以發(fā)現(xiàn),函數(shù)執(zhí)行時(shí),發(fā)現(xiàn)內(nèi)存不足,垃圾回收機(jī)制工作,回收上一個(gè)函數(shù)申請的內(nèi)存,因?yàn)樯蟼€(gè)函數(shù)已經(jīng)執(zhí)行結(jié)束了,內(nèi)存無用可被回收了
所以圖中呈現(xiàn)內(nèi)存使用量的圖表就是一條橫線過去,中間出現(xiàn)多處豎線,其實(shí)就是表示內(nèi)存清空,再申請,清空再申請,每個(gè)豎線的位置就是垃圾回收機(jī)制工作以及函數(shù)執(zhí)行又申請的時(shí)機(jī)
場景二:在某個(gè)函數(shù)內(nèi)申請一塊內(nèi)存,然后該函數(shù)在短時(shí)間內(nèi)不斷被調(diào)用,但每次申請的內(nèi)存,有一部分被外部持有
// 點(diǎn)擊按鈕,就執(zhí)行一次函數(shù),申請一塊內(nèi)存 var arr = []; startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1); arr.push(b); });
看一下跟第一張圖片有什么區(qū)別?
不再是一條橫線了吧,而且橫線中的每個(gè)豎線的底部也不是同一水平了吧
其實(shí)這就是內(nèi)存泄漏了
我們在函數(shù)內(nèi)申請了兩個(gè)數(shù)組內(nèi)存,但其中有個(gè)數(shù)組卻被外部持有,那么,即使每次函數(shù)執(zhí)行完,這部分被外部持有的數(shù)組內(nèi)存也依舊回收不了,所以每次只能回收一部分內(nèi)存
這樣一來,當(dāng)函數(shù)調(diào)用次數(shù)增多時(shí),沒法回收的內(nèi)存就越多,內(nèi)存泄漏的也就越多,導(dǎo)致內(nèi)存使用量一直在增長
另外,也可以使用 performance monitor 工具,在開發(fā)者工具里找到更多的按鈕,在里面打開此功能面板,這是一個(gè)可以實(shí)時(shí)監(jiān)控 cpu,內(nèi)存等使用情況的工具,會(huì)比上面只能抓取一段時(shí)間內(nèi)工具更直觀一點(diǎn):
梯狀上升的就是發(fā)生內(nèi)存泄漏了,每次函數(shù)調(diào)用,總有一部分?jǐn)?shù)據(jù)被外部持有導(dǎo)致無法回收,而后面平滑狀的則是每次使用完都可以正常被回收
這張圖需要注意下,第一個(gè)紅框末尾有個(gè)直線式下滑,這是因?yàn)?,我修改了代碼,把外部持有函數(shù)內(nèi)申請的數(shù)組那行代碼去掉,然后刷新頁面,手動(dòng)點(diǎn)擊 GC 才觸發(fā)的效果,否則,無論你怎么點(diǎn) GC,有部分內(nèi)存一直無法回收,是達(dá)不到這樣的效果圖的
以上,是監(jiān)控是否發(fā)生內(nèi)存泄漏的一些工具,但下一步才是關(guān)鍵,既然發(fā)現(xiàn)內(nèi)存泄漏,那該如何定位呢?如何知道,是哪部分?jǐn)?shù)據(jù)沒被回收導(dǎo)致的泄漏呢?
如何分析內(nèi)存泄漏,找出有問題的代碼
分析內(nèi)存泄漏的原因,還是需要借助開發(fā)者工具的 Memory 功能,這個(gè)功能可以抓取內(nèi)存快照,也可以抓取一段時(shí)間內(nèi),內(nèi)存分配的情況,還可以抓取一段時(shí)間內(nèi)觸發(fā)內(nèi)存分配的各函數(shù)情況
利用這些工具,我們可以分析出,某個(gè)時(shí)刻是由于哪個(gè)函數(shù)操作導(dǎo)致了內(nèi)存分配,分析出大量重復(fù)且沒有被回收的對象是什么
這樣一來,有嫌疑的函數(shù)也知道了,有嫌疑的對象也知道了,再去代碼中分析下,這個(gè)函數(shù)里的這個(gè)對象到底是不是就是內(nèi)存泄漏的元兇,搞定
先舉個(gè)簡單例子,再舉個(gè)實(shí)際內(nèi)存泄漏的例子:
場景一:在某個(gè)函數(shù)內(nèi)申請一塊內(nèi)存,然后該函數(shù)在短時(shí)間內(nèi)不斷被調(diào)用,但每次申請的內(nèi)存,有一部分被外部持有
// 每次點(diǎn)擊按鈕,就有一部分內(nèi)存無法回收,因?yàn)楸煌獠?arr 持有了 var arr = []; startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1); arr.push(b); });
內(nèi)存快照
可以抓取兩份快照,兩份快照中間進(jìn)行內(nèi)存泄漏操作,最后再比對兩份快照的區(qū)別,查看增加的對象是什么,回收的對象又是哪些,如上圖。
也可以單獨(dú)查看某個(gè)時(shí)刻快照,從內(nèi)存占用比例來查看占據(jù)大量內(nèi)存的是什么對象,如下圖:
還可以從垃圾回收機(jī)制角度出發(fā),查看從 GC root 根節(jié)點(diǎn)出發(fā),可達(dá)的對象里,哪些對象占用大量內(nèi)存:
從上面這些方式入手,都可以查看到當(dāng)前占用大量內(nèi)存的對象是什么,一般來說,這個(gè)就是嫌疑犯了
當(dāng)然,也并不一定,當(dāng)有嫌疑對象時(shí),可以利用多次內(nèi)存快照間比對,中間手動(dòng)強(qiáng)制 GC 下,看下該回收的對象有沒有被回收,這是一種思路
抓取一段時(shí)間內(nèi),內(nèi)存分配情況
這個(gè)方式,可以有選擇性的查看各個(gè)內(nèi)存分配時(shí)刻是由哪個(gè)函數(shù)發(fā)起,且內(nèi)存存儲(chǔ)的是什么對象
當(dāng)然,內(nèi)存分配是正常行為,這里查看到的還需要借助其他數(shù)據(jù)來判斷某個(gè)對象是否是嫌疑對象,比如內(nèi)存占用比例,或結(jié)合內(nèi)存快照等等
抓取一段時(shí)間內(nèi)函數(shù)的內(nèi)存使用情況
這個(gè)能看到的內(nèi)容很少,比較簡單,目的也很明確,就是一段時(shí)間內(nèi),都有哪些操作在申請內(nèi)存,且用了多少
總之,這些工具并沒有辦法直接給你答復(fù),告訴你 xxx 就是內(nèi)存泄漏的元兇,如果瀏覽器層面就能確定了,那它干嘛不回收它,干嘛還會(huì)造成內(nèi)存泄漏
所以,這些工具,只能給你各種內(nèi)存使用信息,你需要自己借助這些信息,根據(jù)自己代碼的邏輯,去分析,哪些嫌疑對象才是內(nèi)存泄漏的元兇
實(shí)例分析
來個(gè)網(wǎng)上很多文章都出現(xiàn)過的內(nèi)存泄漏例子:
var t = null; var replaceThing = function() { var o = t var unused = function() { if (o) { console.log("hi") } } t = { longStr: new Array(100000).fill('*'), someMethod: function() { console.log(1) } } } setInterval(replaceThing, 1000)
也許你還沒看出這段代碼是不是會(huì)發(fā)生內(nèi)存泄漏,原因在哪,不急
先說說這代碼用途,聲明了一個(gè)全局變量 t 和 replaceThing 函數(shù),函數(shù)目的在于會(huì)為全局變量賦值一個(gè)新對象,然后內(nèi)部有個(gè)變量存儲(chǔ)全局變量 t 被替換前的值,最后定時(shí)器周期性執(zhí)行 replaceThing 函數(shù)
- 發(fā)現(xiàn)問題
我們先利用工具看看,是不是會(huì)發(fā)生內(nèi)存泄漏:
三種內(nèi)存監(jiān)控圖表都顯示,這發(fā)生內(nèi)存泄漏了:反復(fù)執(zhí)行同個(gè)函數(shù),內(nèi)存卻梯狀式增長,手動(dòng)點(diǎn)擊 GC 內(nèi)存也沒有下降,說明函數(shù)每次執(zhí)行都有部分內(nèi)存泄漏了
這種手動(dòng)強(qiáng)制垃圾回收都無法將內(nèi)存將下去的情況是很嚴(yán)重的,長期執(zhí)行下去,會(huì)耗盡可用內(nèi)存,導(dǎo)致頁面卡頓甚至崩掉
- 分析問題
既然已經(jīng)確定有內(nèi)存泄漏了,那么接下去就該找出內(nèi)存泄漏的原因了
首先通過 sampling profile,我們把嫌疑定位到 replaceThing 這個(gè)函數(shù)上
接著,我們抓取兩份內(nèi)存快照,比對一下,看看能否得到什么信息:
比對兩份快照可以發(fā)現(xiàn),這過程中,數(shù)組對象一直在增加,而且這個(gè)數(shù)組對象來自 replaceThing 函數(shù)內(nèi)部創(chuàng)建的對象的 longStr 屬性
其實(shí)這張圖信息很多了,尤其是下方那個(gè)嵌套圖,嵌套關(guān)系是反著來,你倒著看的話,就可以發(fā)現(xiàn),從全局對象Window 是如何一步步訪問到該數(shù)組對象的,垃圾回收機(jī)制正是因?yàn)橛羞@樣一條可達(dá)的訪問路徑,才無法回收
其實(shí)這里就可以分析了,為了多使用些工具,我們換個(gè)圖來分析吧
我們直接從第二份內(nèi)存快照入手,看看:
從第一份快照到第二份快照期間,replaceThing 執(zhí)行了 7 次,剛好創(chuàng)建了 7 份對象,看來這些對象都沒有被回收
那么為什么不會(huì)被回收呢?
replaceThing 函數(shù)只是在內(nèi)部保存了上份對象,但函數(shù)執(zhí)行結(jié)束,局部變量不應(yīng)該是被回收了么
繼續(xù)看圖,可以看到底下還有個(gè)閉包占用很大內(nèi)存,看看:
為什么每一次 replaceThing 函數(shù)調(diào)用后,內(nèi)部創(chuàng)建的對象都無法被回收呢?
因?yàn)?replaceThing 的第一次創(chuàng)建,這個(gè)對象被全局變量 t 持有,所以回收不了
后面的每一次調(diào)用,這個(gè)對象都被上一個(gè) replaceThing 函數(shù)內(nèi)部的 o 局部變量持有而回收不了
而這個(gè)函數(shù)內(nèi)的局部變量 o 在 replaceThing 首次調(diào)用時(shí)被創(chuàng)建的對象的 someMethod 方法持有,該方法掛載的對象被全局變量 t 持有,所以也回收不了
這樣層層持有,每一次函數(shù)的調(diào)用,都會(huì)持有函數(shù)上次調(diào)用時(shí)內(nèi)部創(chuàng)建的局部變量,導(dǎo)致函數(shù)即使執(zhí)行結(jié)束,這些局部變量也無法回收
口頭說有點(diǎn)懵,盜張圖(侵權(quán)刪),結(jié)合垃圾回收機(jī)制的標(biāo)記清除法(俗稱可達(dá)法)來看,就很明了了:
- 整理結(jié)論
根據(jù)利用內(nèi)存分析工具,可以得到如下信息:
- 同一個(gè)函數(shù)調(diào)用,內(nèi)存占用卻呈現(xiàn)梯狀式上升,且手動(dòng) GC 內(nèi)存都無法下降,說明內(nèi)存泄漏了
- 抓取一段時(shí)間的內(nèi)存申請情況,可以確定嫌疑函數(shù)是 replaceThing
- 比對內(nèi)存快照發(fā)現(xiàn),沒有回收的是 replaceThing 內(nèi)部創(chuàng)建的對象(包括存儲(chǔ)數(shù)組的 longStr 屬性和方法 someMethod)
- 進(jìn)一步分析內(nèi)存快照發(fā)現(xiàn),之所以不回收,是因?yàn)槊看魏瘮?shù)調(diào)用創(chuàng)建的這個(gè)對象會(huì)被存儲(chǔ)在函數(shù)上一次調(diào)用時(shí)內(nèi)部創(chuàng)建的局部變量 o 上
- 而局部變量 o 在函數(shù)執(zhí)行結(jié)束沒被回收,是因?yàn)?,它被?chuàng)建的對象的 someMethod 方法所持有
以上,就是結(jié)論,但我們還得分析為什么會(huì)出現(xiàn)這種情況,是吧
其實(shí),這就涉及到閉包的知識(shí)點(diǎn)了:
MDN 對閉包的解釋是,函數(shù)塊以及函數(shù)定義時(shí)所在的詞法環(huán)境兩者的結(jié)合就稱為閉包
而函數(shù)定義時(shí),本身就會(huì)有一個(gè)作用域的內(nèi)部屬性存儲(chǔ)著當(dāng)前的詞法環(huán)境,所以,一旦某個(gè)函數(shù)被比它所在的詞法環(huán)境還長的生命周期的東西所持有,此時(shí)就會(huì)造成函數(shù)持有的詞法環(huán)境無法被回收
簡單說,外部持有某個(gè)函數(shù)內(nèi)定義的函數(shù)時(shí),此時(shí),如果內(nèi)部函數(shù)有使用到外部函數(shù)的某些變量,那么這些變量即使外部函數(shù)執(zhí)行結(jié)束了,也無法被回收,因?yàn)檗D(zhuǎn)而被存儲(chǔ)在內(nèi)部函數(shù)的屬性上了
還有一個(gè)知識(shí)點(diǎn),外部函數(shù)里定義的所有函數(shù)共享一個(gè)閉包,也就是 b 函數(shù)使用外部函數(shù) a 變量,即使 c 函數(shù)沒使用,但 c 函數(shù)仍舊會(huì)存儲(chǔ) a 變量,這就叫共享閉包
回到這道題
因?yàn)?replaceThing 函數(shù)里,手動(dòng)將內(nèi)部創(chuàng)建的字面量對象賦值給全局變量,而且這個(gè)對象還有個(gè) someMethod 方法,所以 someMethod 方法就因?yàn)殚]包特性存儲(chǔ)著 replaceThing 的變量
雖然 someMethod 內(nèi)部并沒有使用到什么局部變量,但 replaceThing 內(nèi)部還有一個(gè) unused 函數(shù)啊,這個(gè)函數(shù)就使用了局部變量 o,因?yàn)楣蚕黹]包,導(dǎo)致 someMethod 也存儲(chǔ)著 o
而 o 又存著全局變量 t 替換前的值,所以就導(dǎo)致了,每一次函數(shù)調(diào)用,內(nèi)部變量 o 都會(huì)有人持有它,所以無法回收
想要解決這個(gè)內(nèi)存泄漏,就是要砍斷 o 的持有者,讓局部變量 o 能夠正常被回收
所以有兩個(gè)思路:要么讓 someMethod 不用存儲(chǔ) o;要么使用完 o 就釋放;
如果 unused 函數(shù)沒有用,那可以直接去掉這個(gè)函數(shù),然后看看效果:
這里之所以還會(huì)梯狀式上升是因?yàn)椋?dāng)前內(nèi)存還足夠,還沒有觸發(fā)垃圾回收機(jī)制工作,你可以手動(dòng)觸發(fā) GC,或者運(yùn)行一段時(shí)間等到 GC 工作后查看一下,內(nèi)存是否下降到初始狀態(tài),這表明,這些內(nèi)存都可以被回收的
或者拉份內(nèi)存快照看看,拉快照時(shí),會(huì)自動(dòng)先強(qiáng)制進(jìn)行 GC 再拉取快照:
是吧,即使周期性調(diào)用 replaceThing 函數(shù),函數(shù)內(nèi)的局部變量 o 即使存儲(chǔ)著上個(gè)全局變量 t 的值,但畢竟是局部變量,函數(shù)執(zhí)行完畢,如果沒有外部持有它的引用,也就可以被回收掉了,所以最終內(nèi)存就只剩下全局變量 t 存儲(chǔ)的對象了
當(dāng)然,如果 unused 函數(shù)不能去掉,那么就只能是使用完 o 變量后需要記得手動(dòng)釋放掉:
var unused = function() { if (o) { console.log("hi") o = null; } }
但這種做法,不治本,因?yàn)樵?unused 函數(shù)執(zhí)行前,這堆內(nèi)存還是一直存在著的,還是一直泄漏無法被回收的,與最開始的區(qū)別就在于,至少在 unused 函數(shù)執(zhí)行后,就可以釋放掉而已
其實(shí),這里應(yīng)該考慮的代碼有沒有問題,為什么需要局部變量存儲(chǔ),為什么需要 unused 函數(shù)的存在,這個(gè)函數(shù)的目的又是什么,如果只是為了在將來某個(gè)時(shí)刻用來判斷上個(gè)全局變量 t 是否可用,那么為什么不直接再使用個(gè)全局變量來存儲(chǔ),為什么選擇了局部變量?
所以,當(dāng)寫代碼時(shí),當(dāng)涉及到閉包的場景時(shí),應(yīng)該要特別注意,如果使用不當(dāng),很可能會(huì)造成一些嚴(yán)重的內(nèi)存泄漏場景
應(yīng)該銘記,閉包會(huì)讓函數(shù)持有外部的詞法環(huán)境,導(dǎo)致外部詞法環(huán)境的某些變量無法被回收,還有共享一個(gè)閉包這種特性,只有清楚這兩點(diǎn),才能在涉及到閉包使用場景時(shí),正確考慮該如何實(shí)現(xiàn),避免造成嚴(yán)重的內(nèi)存泄漏
總結(jié)
到此這篇關(guān)于js內(nèi)存泄漏場景、如何監(jiān)控及分析的文章就介紹到這了,更多相關(guān)js內(nèi)存泄漏場景監(jiān)控內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Function.prototype.call.apply結(jié)合用法分析示例
昨天在網(wǎng)上看到一個(gè)很有意思的js面試題:var a = Function.prototype.call.apply(function(a){return a;}, [0,4,3]);alert(a); 分析步驟如下,感興趣的朋友可以參考下哈2013-07-07three.js利用gpu選取物體并計(jì)算交點(diǎn)位置的方法示例
這篇文章主要給大家介紹了關(guān)于three.js利用gpu選取物體并計(jì)算交點(diǎn)位置的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用three.js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11JavaScript數(shù)學(xué)對象Math操作數(shù)字的方法
這篇文章主要為大家介紹了JavaScript數(shù)學(xué)對象Math操作數(shù)字的方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Hammer.js+輪播原理實(shí)現(xiàn)簡潔的滑屏功能
這篇文章主要介紹了Hammer.js+輪播原理實(shí)現(xiàn)簡潔的滑屏功能的相關(guān)資料,需要的朋友可以參考下2016-02-02javascript創(chuàng)建頁面蒙板的一些知識(shí)技巧總結(jié)
javascript創(chuàng)建頁面蒙板的一些知識(shí)技巧總結(jié)...2007-08-08B/S開發(fā)中常用javaScript技術(shù)與代碼
B/S開發(fā)中常用javaScript技術(shù)與代碼...2007-03-03JS簡單實(shí)現(xiàn)移動(dòng)端日歷功能示例
這篇文章主要介紹了JS簡單實(shí)現(xiàn)移動(dòng)端日歷功能的方法,涉及javascript針對日期與時(shí)間的操作及顯示相關(guān)技巧,需要的朋友可以參考下2016-12-12