Loading…

最近公司里那个娘炮各种找麻烦,心烦心躁,只能在这里写写东西发泄一下了。

楔子

Once upon a time,在我年幼、无知沉迷于拖VB控件不能自拔的那个岁月,其实想想那会儿似乎是我第一次接触JavaScript这门语言。老实说,当初对这门语言实际上多有厌烦。和大多数人一样,彼时深受于各种浏览器弹窗的困扰(现在都升级成点击挟持了)。但由于参加学校某个比赛项目的缘故,需要在网页上增加一些“动态”元素,那么首当其冲的就是增加一个轮播效果图。

请原谅我年幼时的无知,那时候只会ASP和VB6,因此不得不在百度中摸爬滚打般的学习JavaScript,而在我轮番的搜索中,其中有这样一段话,对于如今的我仍然记忆犹新:

JavaScript是一门弱类型语言,而且用不用var都能声明变量。

当然,随着08年以后Web端的飞速发展,时至今日,大家都知道这段阐述的前半句言之凿凿,可后半句却大错特错。如今,无论新手老鸟都知道var的一个关键特性:不以var修饰的变量声明会自动在global对象中注册,由此导致全局变量污染。

但全局变量污染并不是唯一的影响,而另一个可能造成的问题则是——Dom Leak,Dom碎片引起的内存泄露。

Dom Leak

BE在最早设计JS的时候就是为了简便html文档流的操作,也因此开发了dom接口。而随着近来Mv*框架的大行其道,人们开始越来越多的把传统B/S架构中的VIEW层有意识的放到客户端来做,也因此,dom与JS的互动愈发频繁,但正如我上文提到的,我们在操作Dom的同时,也许不经意间就造成了泄露。

诚如轮子哥所言,很多性能问题不到Profiling阶段,你是无法感知的。以这个著名的案例来说,有如下结构文档和代码

  var select = document.querySelector; 
  var treeRef = select("#tree"); 
  var leafRef = select("#leaf"); 
  var body = select("body"); 
  body.removeChild(treeRef);  

通常情况下,程序员会下意识的以为div#tree已然会移除,并且被回收。但事实上,它们并没有被GC,而是处于下图所展示的Detached状态。

而原因就在于这两句代码。

var treeRef = select("#tree");  
var leafRef = select("#leaf");  

在据我所知的整个JavaScript世界中,除了ES6中的WeakMap,所有的引用关系都是强引用。而这句代码就造成了dom结构与变量之间的引用关系,因此dom tree在移除body后依然只能处于detached状态,无法被回收。

而与此同时,修复的方法其实也很简单,顾名思义,解除引用就可以了。

  var select = document.querySelector; 
  var treeRef = select("#tree"); 
  var leafRef = select("#leaf"); 
  var body = select("body"); 
  body.removeChild(treeRef); 
  leafRef=null;
  treeRef=null;

实际上在做这一系列的研究时,我一直在想,如此这般的话dom leak本身会非常隐蔽,如果不对工程代码进行profiling的话,肉眼几乎无法识别。有什么办法可以又不增加人力成本,又避免dom leak呢?

一觉醒来,恍然大悟。如上文所言,var的关键特性就是确定变量的local scope,也就是作用域,除非函数内有诸如闭包等特性的利用,函数在执行完毕后就会对内部的变量解绑并且回收,因此哪怕函数内的变量与dom存在操作关系,对变量的回收还意味着对dom的引用解绑,而detached dom tree也将得以GC。(循环引用另作他说)

所以,var的重要性不仅仅在于对全局变量的侵略,更在于浏览器引擎能否良好的在App中运行GC,来保证始终流畅、反应迅速的用户体验。

另外,在具体的工程实践中,与dom有关的操作也应该尽可能的使其所在函数的颗粒度足够的小,来保证函数本身可以及时被回收,而类似以下代码则不推荐使用。

(function(){
   ...
   //业务代码或者框架代码
   var node = document.createElement('div')//直接在大闭包的root作用域操作dom
   ...
}())
About the author
comments powered by Disqus