setTimeout与js引擎的异步执行

来源:互联网 发布:墙漆 知乎 编辑:程序博客网 时间:2024/04/27 15:57

从岁月如歌那里看到一篇文章,是说“大数组的分时优化处理”,讲述了如何使用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的最小时间片。此外,浏览器是顺序调用堆栈中的函数,比如图:

setTimeout1

图中可以看到,js引擎过滤js代码的时候,将代码段进行拆分,在js修改dom节点之后进跟着会render一下页面,好让页面看到js操作dom后的结果,这是合情合理的,当然,通常情况下我们希望浏览器是按照这样固定的逻辑执行,而且大部分浏览器在多数情况下也是这么做的,然而有时候会有偏差。比如,当js中的若干个改变dom节点的操作相互紧临,而且每两个操作dom节点的操作共花费的时间小于浏览器处理单进程的最小时间片,这时不同浏览器的表现就出现不一致,但通常在浏览器内存比较充裕的情况下,浏览器会将这若干个连续的dom操作会按照浏览器最小分片时间进行分割,即可能两三个dom操作的时间和大于浏览器处理单进程的最小时间片,这是两三个dom操作后浏览器才渲染一次页面,但在浏览器内存吃紧的情况下,有些浏览器会将这些相互时间间隔小于浏览器的单进程时间片的dom操作集合,合并到一次浏览器操作,并作为一次堆栈调度,这样的话,浏览器会等待这些紧邻的dom操作结束后一次渲染页面,如图:

setTimeout2

当然,不同浏览器对单进程最小分片都有不同的尺寸定义,而且不同浏览器也处于对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的体验更佳,看这个例子就明白了。

<!doctype html>
<html dir="ltr" lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
</head>
<body>
<h1>例子1</h1>
<button id="clickme1">没有setTimeout的连续dom操作</button>
<button id="clickme2">定义了setTImeout的连续dom操作</button>
<div style="width:1px;background:red;overflow:hidden;height:22px;" id="jd">进度</div>
<div  id='alink' style="height:200px;overflow:auto;">logs here</div>
<script type="text/javascript" src="http://cn.yimg.com/i/yui/3.0.0b1/build/yui/yui-min.js"></script>
<script>
<!--
YUI().use('node', function(Y){
//定义暴力程度
var rang = 300;
var handlePercent = function(k){
var w = Math.floor(k/rang*100);
Y.Node.get('#jd').setStyle('width',w+'px');
};
Y.on('click',function(e){
Y.Node.get('#alink').set('innerHTML','');
var k = 0;
var t = new Date();
for(var i = 0;i<rang;i++){
Y.Node.get('#alink').set('innerHTML',Y.Node.get("#alink").get('innerHTML')+'<br />'+(k++));
handlePercent(k);
Y.Node.get('#jd').set("innerHTML",(Number(new Date())-Number(t)));
}
},'#clickme1');
Y.on('click',function(e){
Y.Node.get('#alink').set('innerHTML','');
var k = 0;
var t = new Date();
var foo = function(){
Y.Node.get('#alink').set('innerHTML',Y.Node.get("#alink").get('innerHTML')+'<br />'+(k++));
handlePercent(k);
Y.Node.get('#jd').set("innerHTML",(Number(new Date())-Number(t)));
if(k != rang)setTimeout(arguments.callee,0);
};
setTimeout(foo,0);
},'#clickme2');
});
//-->
</script>
</body>
</html>
</html>

<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,但仍然被放到了一个另外的堆栈调用中,在事件结束后才调用。

setTimeout2

明白了这个过程,也就明白了为什么使用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就够了。

原创粉丝点击