一个疑似bug,让我见识console.log的输出快照和事件数据的自动回收

499 阅读3分钟

前言

大家好,我是抹茶。
今天发现了一个特别有意思的事情,进行console.log(obj),展开前的数据和展开后的数据存在差异。原本以为是数据类型处理上的bug,但这里面涉及了console.log对引用类型的访问处理、浏览器的回收机制。且听故事娓娓道来。

故事的背景

需要让用户在网页上粘贴图片,然后再上传服务器,会使用到HTML 的paste事件。监听paste事件,从event中拿到粘贴板的内容数据,并用console.log打印输出。 核心代码如下:

    handlePaste (event) {
      console.log('event', event);
      
      const clipboardItems = event?.clipboardData?.items || [];

      console.log('循环前长度', clipboardItems.length);
      console.log('循环前对象', clipboardItems, '\n');
      
      for (let idx = 0; idx < clipboardItems.length; idx++){
          console.log('item', clipboardItems[idx]);
      }

     
      console.log('循环后长度', clipboardItems.length);
      console.log('循环后对象', clipboardItems)
      
    },

console.log()打印引用类型之坑

有意思的地方来了,DataTransferItemList在展开前显示长度为1,在展开后显示长度为0,这是为什么呢?

image.png

查阅了诸多资料,原因在于用console.log()打印引用类型时,访问输出的是对象的快照,而在展开时访问的是对象的最新值快照和最新值之间可能存在差异。用debugger进行调试,可以规避这种差异

image.png

再来看看其他的case

const obj = {
  num: 1,
  text:'我劝天公重抖擞'
}
console.log(obj);
obj.num++;
obj.text = '不拘一格降人才';
console.log(obj);

在谷歌浏览器中结果如下:

image.png

在Safari浏览器结果如下:

image.png

在火狐浏览器结果如下:

image.png

可见,不同的浏览器对console.log()有自己的实现规则,因为并没有什么规范或一组需求指定console.*方法族如何工作--它们并不是JavaScript正式的一部分,而是由宿主环境添加到JavaScript中的。

尤其要指出的事,在某些条件下,某次浏览器的console.log()并不会把传入的内容立即输出。出现这种情况的主要原因是,在许多程序(不只是JavaScript)中,I/O是非常低速的阻塞部分。所以,(从页面/UI的角度来说)浏览器在后台异步处理控制台I/O能够提高性能,这时用户甚至可能根本意识不到其发生。

根据《你不知道的JavaScript 中卷》内容,最好的选择是在JavaScript调试器中使用断点,而不要依赖控制台输出。次优的方案是把对象序列化到一个字符串中,以强制执行一次“快照”,比如通过JSON.stringify()。

从这三大浏览器的共同之处在于,对于引用类型,打印时输出的是它的快照,在展开时显示的是最新的值。

回到第一个case的粘贴事件上,为何DataTransferItemList的最新值会变变成length为0呢?

浏览器针对临时事件的垃圾回收

paste事件是一个一次性事件,在事件触发完成后,事件产生的数据的数据大概率就用不上了(需要用可以用自定义的js变量存储起来),浏览器会对这样的数据进行垃圾回收,也就是释放掉这部分的内存。

粘贴事件的垃圾回收机制.png

总结

从这个案例可以得出两个点:

  • 针对引用类型,console.log()打印输出的是对象的快照(默认内容是折叠的),在展开时显示的是对象的最新值,保险期间,应该用debugger调试定位对象的值,console.log可能会引发未知bug。

  • 针对类似粘贴这样的一次性事件,浏览器会在事件触发完成后,自动回收释放掉事件数据的占用的内存。

demo的源码

// vue文件
<template>
  <div>
    <div id="app">
      <div contenteditable="true" @paste="handlePaste" class="paste-area" 
      >
        粘贴你的图片到这里
      </div>
      
    </div>

  </div>
</template>
<script>

export default {
  data() {
    return {
      imageSrc: ''
    }
  },
  mounted () {
   
  },
  methods: {
    handlePaste (event) {
      console.log('event', event);
      
      const clipboardItems = event?.clipboardData?.items || [];
      debugger

      console.log('循环前长度', clipboardItems.length);
      console.log('循环前对象', clipboardItems);

      console.log('\n');
      
      for (let idx = 0; idx < clipboardItems.length; idx++){
          console.log('item', clipboardItems[idx]);
      }

      console.log('\n');

     
      console.log('循环后长度', clipboardItems.length);
      console.log('循环后对象', clipboardItems)
      
    },

   
  }
}
</script>
<style>
.paste-area {
  border: 2px dashed #ccc;
  padding: 20px;
  text-align: center;
  margin: 20px;
  cursor: text;
  min-height: 100px;
  width:200px;
  height:200px;
  img{
    width: 100%;
    height:100%
  }
}

</style>