4 实践——高效的JavaScript代码
4.1 编程方式
关于如何使用JavaScript语言来编写高效的代码,有很多铺天盖地的经验分享,以及很多特别好的建议,读者可以搜索相关的词条,就能获得一些你可能需要的结果。同时,本节希望结合前面介绍的各种引擎内部的技术,按照特定的类别为读者归纳一些方式和方法,让我们从以下几个方面来解读它们。
- 类型 。因为JavaScript的类型是在动态时候确定的,这给引擎带来很大的问题。同时对于某个函数来说,V8和JavaScriptCore都使用了隐藏类和内嵌缓存技术来加速对象和属性的访问,所以对于该函数只是使用某个类型的对象或者较少类型,以此减少缓存失误的机率从而提高性能。同时,对于数组,尽量使用存放相同类型的数据,这样可以通过偏移位置来访问它们。在目前的众多技术中,比较突出的就是asm.js,它主要是在JavaScript中显示标记一些类型,这样可以让JavaScript引擎能够准确判断对象的类型,从而生成优化的代码。目前Firefox项目中的JavaScript引擎SpiderMonkey已经(正在做)内置支持该JavaScript文件,详情请查找asm.js。
- 数据表示 。因为一些简单类型的数据直接保存在句柄中,这能够有效地减少寻址时间和内存的使用。但是,因为使用了一部分位(特别对于V8引擎)来表示,所以整数表示范围缩小,如果使用较大的整数,那么就需要使用堆来保存。同时,对于数值来说,只要能够使用整数的,尽量不要使用浮点类型。
- 内存 。有效使用内存能够显著地提高代码的性能。对于使用垃圾回收的语言来说,并不是意味着没有内存泄露的问题,这就需要即时回收不需要使用的内存。简单的做法就是对引用不再使用的对象的变量设置为空(a = null)。另外一个方法跟类型有关,通过引入delete关键字,代码可以使用“delete a.x”来删除一个对象,这虽然可以减少内存的使用,但是因为使用了隐藏类,这种情况下可能需要新建隐藏类,所以这会带来一些复杂的额外操作。
- 优化回滚 。如前面介绍的,不要书写出触发出现优化回滚的代码,否则会大幅降低代码的性能。在执行多次之后,不要出现修改对象类型的语句。这说起来可能有些难,但实际上,如示例代码9-5之类的用法即可。
- 新机制 。使用JavaScript引擎或者是渲染引擎提供的新机制和新接口,如前面介绍的requestAnimationFrame等接口,这样可以有效减少JavaScript引擎的额外负担。另外,可以使用WebWorker等JavaScript并发技术来提升引擎并发处理能力。
4.2 例子
在浏览器中,JavaScript引擎和渲染引擎WebKit需要协同工作才能达到一个好的效果,结合这二者,这一小节来介绍一个简单的例子,就是后来被引入的新的JavaScript接口requestAnimationFrame,以此来解释它是如何解决两者之间一些比较难以处理的问题的,以及它给Web前端开发者带来的思考。
接触过JavaScript的读者应该有过了解或者使用setTimeout或setInterval的经历,其功能是在每个时间间隔之后一次性或者重复多次执行一段JavaScript代码(称为回调函数),以完成特定的动画要求。但是,这里面多少还有些疑问。
- 时间间隔应该设置为多少才合适呢?跟屏幕的分辨率有关系吗?
- 设置的时间间隔会按照预想的执行吗?动画会被平滑地显示出效果吗?
- 回调函数是复杂的好还是简单的好呢?应该如何编写才能效率高呢?
- 与平台和浏览器相关吗?如何适应不同操作系统和浏览器呢?
这些问题对setTimeout和setInterval来说很重要。对主循环机制和渲染机制有一定了解的读者来说,上面这几条其实是非常难做到的,哪怕是较为接近理想的结果也很难达到。
幸运的是,总是有聪明的人来帮助大家解决难题。对问题提出一个漂亮解决方案的是Mozilla的Robert O'Callahan。他的灵感和依据来源于CSS。CSS能够知道动画什么时候发生,所以能够较为准确地知道什么时候该刷新用户界面。对于JavaScript来说,是不是也可以应用类似的机制呢?答案是肯定的。其做法是增加一个新的函数requestAnimationFrame,该函数告诉浏览器JavaScript想发起一个动画帧,然后在动画帧绘制之前,需要做一些动作,这样浏览器可以根据需要来优化自己的消息循环机制和调用时间点,以达到较好的平衡效果。
WebKit中setTimeout和setInterval的实现机制是类似的,区别在于后者是重复性的,如图9-28所示的类关系。
图9-28 WebKit中的计时器等相关类
WebKit会为DOM树中的每个setTimeout和setInterval调用创建一个DOMTimer,而后该对象会由存储TLS(Thread Local Storage)中的ThreadTimers负责管理,其内部其实是一个最小堆,每次将超时时间设置为最小的。同时,时间相同的计时器可以合并。当计时器超时后,Chromium将清除该计时器对象,同时调用相应的回调函数,回调函数通常会更新页面的样式和布局,这会触发重新计算布局,从而触发立即重新绘制一个新帧。结合上面的描述,这里大致总结一下setTimeout和setInterval的不足。
- setTimeout和setInterval从不考虑浏览器内部发生了其他什么事,它们只要求浏览器在某个时间之后来调用回调函数,无论浏览器很繁忙或者页面被隐藏(虽然某些浏览器做了这方面的优化,如Chromium)。
- setTimeout和setInterval只是要求浏览器做什么,而不管浏览器能不能做到(如主循环有很多事件需要处理),这有点强人所难,而且会带来极大的资源浪费。例如屏幕的刷新率是60Hz,但是设置的时间间隔是5毫秒,其实对用户来说,他们根本看不到这些变化,但却额外需要消耗更多的CPU资源,太不环保了。
- setTimeout和setInterval可能是出于编程风格方面的考虑。如果每一帧在不同的代码处需要设置回调函数,一个方法是将这些代码统一到一个地方,但是这有点勉为其难,另一个方法是分别用setInterval设置它们,这个方法的问题是,浏览器可能需要计算更多次,刷新更多次的屏幕。
现在再来看看requestAnimationFrame是如何解决这些不足之处的呢?其原理就是其会申请绘制下一帧,至于什么时候还不知道,都是由浏览器决定,浏览器只需要在绘制下一帧前执行其设置的回调函数,完成JavaScript代码对动画所做的设置和逻辑即可。基本过程如下。
- JavaScript调用requestAnimationFrame,因而相应地,Webkit和Chromium会调度一个需要绘制下一帧的事件,该事件会将requestAnimationFrame的调用上下文和回调函数记录下来。
- 上面的请求会触发Chromium更新页面内容的事件,该事件被mainloop调度处理后,会检查是否需要调用动画的相关处理,因为有动画需要处理,所以会依次调用那些回调函数,JavaScript引擎会更新相应的CSS属性或者DOM树修改。
- Chromium触发重新计算布局(参看布局章节),更新自己的Renderer树,而后绘制,完成一帧的渲染。
上面这些描述会给Web前端开发者们在编写JavaScript代码时带来哪些思考和便利呢?
- 回调函数不能太大,不能占用太长时间,否则会影响页面的响应和绘制的频率。
- requestAnimationFrame不需要设置间隔时间,不同刷新率的间隔时间可能不一样,这完全由浏览器来控制,而不需要JavaScript代码的开发者们操心。
- 回调函数无需合并,开发者们可以在任意位置设置回调函数,它们可以被浏览器集中处理,而无需有统一的入口。
一个新的JavaScript接口可以带来很不错的处理方式,以此来平衡JavaScript引擎和渲染引擎之间的关系,并且能够有效帮助那些利用JavaScript和HTML5技术来实现动画的开发者们,这一点值得我们思考。
4.3 未来
因为历史的局限性,JavaScript最初的时候并不合适用来开发大工程和性能要求非常高的场景,所以开发者编写的代码对性能要求也不是很高。但是,目前的发展趋势是需要很高的性能,为此,仅仅依靠任何一方是没有办法来达到此目标的。笔者认为,今后为了高效的JavaScript代码性能,至少需要以下三个方面的努力,而且,就目前而言,它们也都在不停地向前发展。
首先是JavaScript语言和规范的发展。目前虽然规范定义的WebWorker在一定程度上能够并发,但是能力非常有限,而且两者之间只能通过有限的方式来通信(这个技术是由W3C组织引入的)。如果能够在ECMAScript标准中推动并行JavaScript能力,这绝对是一个大胆而又令人神往的想法。目前,一些大公司或者组织已经在推动并行JavaScript,希望未来有快速的发展,能够带领JavaScript真正进入并行时代。
其次是JavaScript引擎技术的发展和创新。一个简单的例子就是,V8不停地将之前用在其他编译器的技术带入到JavaScript引擎中来,同时自身也创造一些新的方法。据笔者目前观察得知,基本每个V8版本的升级都会带来性能上的提高,大家有理由相信,在这场JavaScript引擎大战中,各个引擎都会不停地提升技术以提升性能。
最后是同Web前端开发者相关的,那就是关于编写高效的JavaScript代码。结合语言的新能力和引擎技术的不断发展,要根据它们的特点,使用新技术和回避一些会对引擎带来重大性能伤害的用法。目前还没有这方面的系统介绍,希望未来能够有更多帮助开发者提高代码效率的使用方法被共享出来。