前言
相信各位大佬都遇到过这样一种场景:我们给页面做一个精美的点击效果,例如点击页面任意地方产生一个渐渐扩散的涟漪,扩散到一定大小后消失。在js中我们可以给window
添加一个点击事件,点击后创建一个行内元素(假设是span
元素)挂载到body
上,再利用setTimeout
和element.remove
延时清除这个创建的span
对象,最后给相应的元素添加一点样式和动画就完成效果了。但是这个span
标签对象真的消除了吗?
动画实现
<style>
body {
margin: 0;
padding: 0;
background-color: rgb(255, 122, 228);
}
span {
position: fixed;
border-radius: 50%;
width: 40px;
height: 40px;
transform: translate(-50%, -50%);
background-color: rgb(0, 162, 255);
animation: animation 2s linear infinite;
}
@keyframes animation {
0% {
width: 0px;
height: 0px;
opacity: 0.5;
}
100% {
width: 16vw;
height: 16vw;
opacity: 0;
}
}
</style>
<body id="body">
<script>
let body = document.getElementById('body')
window.addEventListener('click', e => {
let x = e.clientX
let y = e.clientY
let eleSpan = document.createElement('span')
eleSpan.style.left = `${x}px`
eleSpan.style.top = `${y}px`
body.appendChild(eleSpan)
setTimeout(() => {
eleSpan.remove()
console.log(eleSpan);
}, 2000);
})
</script>
</body>
我在eleSpan.remove
之后打印了eleSpan
,结果到底如何呢,会是undefined
吗?这就来试试:
可以清晰地看到,我在页面上点击了两次,并且在每次点击延时两秒之后,控制台都打印了span
标签对象,这说明eleSpan.remove
只是将span
标签从DOM
节点中移除了,并没有在内存中将其删除。
危害和解决方法
上述例子中,我只是在页面中点击了两次,假如各位大佬将来的项目上线,每天都要受到成千上万次的点击,内存中保存了成千上万个这样的span
标签对象,那岂不是瞬间卡死。这时必定有大佬就要说了,在这bb这么久,直接把eleSpan
赋值为null
不就行了。我只能说,大佬就是大佬,果然我还是班门弄斧了。确实是这样解决的。
setTimeout(() => {
span.outerHTML = null
span = null
console.log(span);
}, 2000);
将eleSpan
设为null
之后,就不仅仅是移除了该节点,而是手动实现了一次垃圾回收。
这里稍微有一点需要注意的地方,在将eleSpan
赋值为null
之前,需要先将eleSpan.outerHTML
赋值null
,不然会出现eleSpan
设为null
后,DOM
节点上的span
元素依然还在。具体原因我也不清楚,欢迎各位大佬评论区留言指教!
当span元素被另一个对象引用
在将创建出来的span
元素被回收之前,我创建一个新的对象storage
,把span
标签对象设为其属性dom
的值,并将storage.dom
作为节点挂载到body
上,代码如下:
let storage = {
dom: eleSpan
}
body.appendChild(storage.dom)
let str = '打印storage.dom:'
setTimeout(() => {
eleSpan.outerHTML = null
eleSpan = null
console.log(`打印eleSpan:${eleSpan}`);
console.log(str, storage.dom);
}, 2000);
再来看看效果吧
哦吼,只有eleSpan
被回收了,storage.dom
仍然存在于内存之中,也就是说,在这种情况下,手动回收内存失败了。这到底是为什么呢?原因如下:
原理
对象的引用是计数引用,当一个引用类型被创建时,它在内存空间中被引用次数的计数就会加一,上述案例中span
标签对象被创建时计数就已经加了一,后来被storage.dom
引用时又加了一,总计数为2,把eleSpan
赋值为null
之后,计数减一,还剩一。
如何解决
解决这个问题也很简单,只要将storage.dom
赋值为null
,让span
的引用计数再减一即可。
js的存储和垃圾回收机制
存储机制
在JavaScript中有两种内存空间,栈内存和堆内存。栈内存比较小,一般用于存储简单数据类型,可以快速地完成执行栈的切换;堆内存很大,复杂数据类型都存储在堆内存中,当我们把一个复杂数据类型赋值给一个变量时,例如var obj = {}
,JavaScript做了如下几件事情:
- 在栈内存中声明一个名为
obj
的变量 - 在堆内存中开辟一块新的内存空间存储
{}
- 将
obj
的指针指向堆内存中{}
存放的空间 这样obj
就对{}
的内存空间产生了一个强引用关系。如果将obj
赋值为null
,就解除了这个强引用关系。 由此可知,在上述案例中,storagr.dom = eleSpan
执行后,storagr.dom
就指向了span
标签对象在堆内存的存储空间,span
标签对象同时eleSpan
和storagr.dom
被两个变量保持引用关系,当span = null
执行后,只解除了变量eleSpan
对span
标签对象在堆内存存储空间的引用,而storagr.dom
上的引用关系并不会受到影响。
垃圾回收机制
其实JavaScript是有自动内存回收机制的,但由于引用数据类型的特殊性质和闭包的存在,这种自动回收机制就显得不可靠:一般而言,简单数据类型一但结束调用,它在栈内存空间中就会被销毁,在闭包中它的生命周期会得到延长;对于引用数据而言,只要当它在内存中的引用计数为0时才会被回收掉,或者说当所有对它的引用关系都解除之后才会被回收。
垃圾回收很重要
垃圾回收对于性能优化非常重要,因为对于任何设备来说,空间都是有限的、宝贵的,所有关于内存的优化都应该做垃圾回收,做到及时释放内存。
手动调试内存
在node.js
环境下,我们可以通过一些命令来手动调试内存,观察内存的开销。在任意文件下打开终端,执行以下命令:
1. node --expose-gc
进入交互模式
2. process.memoryUsage()
查看内存开销情况
其中heapUsed
表示堆空间被使用的情况。
3. global.gc()
手动执行一次垃圾回收,其中global
是 node.js
中的全局对象。
由此我们不妨在交互模式下做一点小测试:
process.memoryUsage()
/*输出:
{
rss: 26640384,
heapTotal: 4792320,
heapUsed: 4125200,
external: 1722368,
arrayBuffers: 59073
}
*/
let arr = new Array(5 * 1024 * 1024)
process.memoryUsage()
/*输出:
{
rss: 68145152,
heapTotal: 47525888,
heapUsed: 45540456,
external: 1722410,
arrayBuffers: 75459
}
*/
可以看到初始状态heapUsed
的值为4,125,200
,当我声明了一个大小为5MB数组之后,heapUsed
的值变成了45,540,456
,比初始状态的十倍还多。继续测试:
arr = null
global.gc()
process.memoryUsage()
{
rss: 26234880,
heapTotal: 5054464,
heapUsed: 3291664,
external: 1721811,
arrayBuffers: 132797
}
*/
可以看到将arr
设为null
之后,执行global.gc()
进行一次垃圾回收,内存就被释放了,heapUsed
的大小又回到了初始水平。
掌握了这个技巧后大佬们就能进行一些更复杂的测试啦,大佬们不妨试试对上述arr
保持多个引用时,要怎样才能彻底释放内存。
WeakMap
通过前面的赘述,我们不难知道对一个保持多个引用关系的引用数据类型进行垃圾回收是一件复杂又困难的事情,这让我们对内存的性能优化十分不友好。好在巨人的眼镜也是雪亮的,JavaScript的开发者们在ed6中提供了一些新的数据结构,其中有一个名为WeakMap
的就是用来解决垃圾回收问题的。
WeakMap
是一种特殊的Map
结构,相比于Map
,它有如下特性:
WeakMap
的键是不可枚举的WeakMap
的键必须是复杂数据类型WeakMap
的键所指的对象如果没有被引用会被自动垃圾回收掉 原因:WeakMap
的键对其他对象是的引用弱引用关系,当键所指对象在其他地方被回收后,WeakMap
会自动解除和原对象的引用关系。我们不妨在交互模式下(手动调试内存小结中有进入交互模式的详情)做个测试:
let key = new Array(5 * 1024 * 1024)
let map = new WeakMap()
map.set(key, 1)
process.memoryUsage()
/*输出:
{
rss: 68268032,
heapTotal: 47001600,
heapUsed: 45424824, // 8位数
external: 1721770,
arrayBuffers: 157372
}
*/
key=null
global.gc()
process.memoryUsage()
/*输出:
{
rss: 26259456,
heapTotal: 5054464,
heapUsed: 3308112, // 7位数
external: 1721820,
arrayBuffers: 280262
}
*/
从上述代码输出的内存开销情况来看,执行key = null
之后数组数组在堆内存中的存储空间就被回收了,并没有因为map
对其保持引用导致内存无法回收。这就是WeakMap
键保持弱引用关系的作用:相对于强引用关系,弱引用关系不会让被引用对象在堆内存中的引用计数加一。
WeakMap扩展
WeakMap实现私有变量
WeakMap
还能作为实现私有变量的一种方式,代码如下:
const privateData = new WeakMap()
class Person {
constructor(name, age) {
privateData.set(this, { name: name, age: age })
}
getName() {
return privateData.get(this).name
}
getAge() {
return privateData.get(this).age
}
}
const kaSha = new Person('卡莎', 18)
console.log(kaSha.name); // undefined
console.log(kaSha.getName()); // 卡莎
console.log(kaSha.getAge()); // 18
当然这里使用Map
也是能实现效果的,但Map
的键是保持强引用关系,使用Map
可能会造成内存没有自动回收的问题,进而造成信息泄露,因此不建议使用。
结语
JavaScript由于引用数据类型的特殊性质,常常导致无法自动回收内存,这就使得我们常常要手动回收引用数据的内存,这项工作有可能会很困难,因此在es6规范中,js提供了WeakMap
来做引用数据的内存自动回收,当然WeakMap
作用不仅仅是这样,它还具备其他功能,比如实现私有变量
*写文章不易,各位大佬点个赞再走吧🥳😄👍