(转)setTimeout与js引擎的异步执行
从岁月如歌那里看到一篇文章,是说“大数组的分时优化处理”,讲述了如何使用timedChunk来改善用户体验,所谓timedChunk的确可以很大程度改善用户体验,但文章并无介绍这种优化性能方法的深层原因,而且“大数组“的例子会让很多人产生误解,setTimeout的用处不止如此。这里的timedChunk是Nicholas C. Zakas对js引擎单进程使用setTimeout进行hack的一种叫法。John Resig很早就给出了setTimeout工作机制的一种解释,这个解释基本全面的描述了单进程模式的js引擎对setTimeout的处理,并无对所有浏览器的js引擎作详细分析,毕竟并不是所有的js引擎都是单进程。不过这篇文章已经相当权威了。
timedChunk是如何根据setTimeout来优化体验呢?为何setTimeout只能部分优化体验,而不能优化性能?什么时候需要使用setTimeout?如何使用setTimeout对ie作hack?为什么ie的js引擎要对ECMAScript模式作hack?本文将对这些问题一一解答。
首先明确一点,多数js引擎是单进程的解释器,或者说在一个web页面中,js是单进程执行的,所谓单进程,就是浏览器无法在渲染页面的同时执行js,这里说的渲染是将粒度放大的一个“渲染”操作,不论浏览器渲染页面有多快,总会耗费一定的时间,在这个时间端内浏览器干不了其他的事情,就类似在cpu的最小时间片单位中,cpu也只能针对一个任务进行运算。虽然浏览器调度渲染和js线程的时间片长度远大于cpu的最小时间片。此外,浏览器是顺序调用堆栈中的函数,比如图:
图中可以看到,js引擎过滤js代码的时候,将代码段进行拆分,在js修改dom节点之后进跟着会render一下页面, 好让页面看到js操作dom后的结果,这是合情合理的,当然,通常情况下我们希望浏览器是按照这样固定的逻辑执行, 而且大部分浏览器在多数情况下也是这么做的,然而有时候会有偏差。 比如,当js中的若干个改变dom节点的操作相互紧临,而且每两个操作dom节点的操作共花费的时间小于浏览器处理单进程的 最小时间片,这时不同浏览器的表现就出现不一致,但通常在浏览器内存比较充裕的情况下, 浏览器会将这若干个连续的dom操作会按照浏览器最小分片时间进行分割,即可能两三个dom操作的时间和 大于浏览器处理单进程的最小时间片,这是两三个dom操作后浏览器才渲染一次页面,但在浏览器内存吃紧的情况下 ,有些浏览器会将这些相互时间间隔小于浏览器的单进程时间片的dom操作集合,合并到一次浏览器操作, 并作为一次堆栈调度,这样的话,浏览器会等待这些紧邻的dom操作结束后一次渲染页面,如图:
当然,不同浏览器对单进程最小分片都有不同的尺寸定义,而且不同浏览器也处于对js引擎速度的考虑, 会合并若干次dom操作到一次堆栈调度中,多数浏览器对这中js引擎的hack作的很缜密, 尽管会有因为渲染不及时带来的用户体验不佳,但还至少能做到慢吞吞的一边烧着cpu一边render页面, 但ie自作聪明的多作了一些存在严重bug的hack,比如ie中认为没有hasLayout属性的dom节点的js操作不会触发render, 再比如有时dom没有display=block的属性则改变其属性不会触发rander, 因此很多人在写js的时候,常会遇到一些很奇怪的事情,抱怨明明在js中更改了dom节点的属性, 而且更改成功,但浏览器中竟然看不到更改结果。为了改善这种状况,只有一个方法:异步调用。
我们通常理解的异步概念大都来自于ajax,即页面向后端发起请求,这时不应当等待返回结果, 而是继续执行,等有返回的时候就执行回调,这里的回调函数执行的时机是不固定的, 准确说是依赖于后端的返回。在浏览器单进程渲染过程中,将相邻的dom操作做为异步事件, 这样dom操作就会被跳过,等到合适时机再执行dom操作, 这时执行的dom操作已经和当初的逻辑不在一个浏览器单进程时间片中,即不属于一次堆栈调用。 如果将每个dom操作都作为异步事件,那么所有的dom操作都将各自作为一次单独的堆栈调用, 这样的话浏览器则会对这些独立的分片后插入一次渲染操作,这样每次dom操作后都渲染到页面中, 都能被看到了。而setTimeout则可以实现将一个函数作为一次异步调用放到一个独立的堆栈中, 尽管setTimeout的delay是0,也会作为一次异步调用,而每次异步调用结束后都会render页面, 因此就比浏览器批量操作dom后一次render的体验更佳,看这个例子就明白了。
<input type="text" value="a" name="input" onkeydown="alert(this.value)" /> <input type="text" value="a" name="input" onkeydown="var me=this;setTimeout(function(){alert(me.value)},0)" />
我们试一下在文本域的a后添加新的字符串。
其中第二个例子说明了setTimeout和浏览器事件的异步调用。虽然setTimeout的delay是0,但仍然被放到了一个另外的堆栈调用中,在事件结束后才调用。
明白了这个过程,也就明白了为什么使用setTimeout只会改善体验而不会改善性能,setTimeout会多出许多render操作,当然会慢,我给的例子中很明显可以看出,异步渲染页面的时间多耗费了将近一倍,但和用户体验的提升相比,还是值得的。因此,当js逻辑中有大量的循环造成连续的修改dom节点,这时就应当使用setTimeout来改善体验。如果在调试ie的时候发现没有及时render页面,可以使用setTimeout来hack,原因如上。ie对连续dom操作的hack大概是处于性能考虑,windows的性能本来就不敢恭维,再跑ie就更慢,各种网测也足以证明ie的js引擎是最糟糕的,因此ie要搞一些css expression和haslayout这些怪胎出来作性能hack就不足为怪了。回头看岁月入歌的分析,那个所谓的25ms是对浏览器单进程调度最小时间片的一种猜测值,这个25ms应当和连续dom操作的个数有关,所以这个猜测值意义并不大。如果将每个dom操作都setTimeout包住,delay设为0就够了。
一些有用的链接