分享

认识延迟时间为 0 的 setTimeout

 chanvy 2008-11-18

由 John Resig 的 How JavaScript Timers Work 可以知道,现有的 JavaScript 引擎是单线程处理任务的。它把任务放到队列中,不会同步去执行,必须在完成一个任务后才开始另外一个任务。

让我们看看我之前的文章:JavaScript的9个陷阱及评点,在第 9 点 Focus Pocus 中提到的问题。原作者对这个认识有所偏差,其实不只是 IE 的问题,而是现有 JavaScript 引擎对于线程实现的问题(关于线程,我的概念其实不多,如果不对,希望读者多多指教)。我们通过一个例子来说明,请访问 http:///lab/settimeout.html. 我们来看 1 和 2。如果你能看看源代码,会发现我们的任务很简单,就是给文档增加一个 input 文本框,并聚焦和选中。请现在分别点击一下,可以看到,1 并没有能够聚焦和选中,而 2 可以。它们之间的区别在于,在执行

input.focus();
input.select();

时, 2 多了一个延迟时间为 0 的 setTimeout 的外围函数,即:

setTimeout(function(){
input.focus();
input.select();
}, 0);

按照 JavaScript: The Definitive Guide 5th 的 14.1 所说:

在实践中,setTimeout 会在其完成当前任何延宕事件的事件处理器的执行,以及完成文档当前状态更新后,告诉浏览器去启用 setTimeout 内注册的函数。

其实,这是一个把需要执行的任务从队列中跳脱的技巧。回到前面的例子,JavaScript 引擎在执行 onkeypress 时,由于没有多线程的同步执行,不可能同时去处理刚创建元素的 focusselect 事件,由于这两个事件都不在队列中,在完成 onkeypress 后,JavaScript 引擎已经丢弃了这两个事件,正如你看到的例子 1 的情况。而在例子 2 中,由于setTimeout可以把任务从某个队列中跳脱成为新队列,因而能够得到期望的结果。

这才是延迟事件为 0 的setTimeout的真正目的。在此,你可以看看例子 3,它的任务是实时更新输入的文本,现在请试试,你会发现预览区域总是落后一拍,比如你输 a, 预览区并没有出现 a, 在紧接输入 b 时, a 才不慌不忙地出现。其实我们是有办法让预览区跟输入框同步地,在此我没有给出答案,因为上面所说的,就是解决思路,try it yourself!

18 Responses to “认识延迟时间为 0 的 setTimeout”

  1. 现学现卖 Says:

    get(’input’).onkeypress = function(){
    setTimeout(function() {get(’preview’).innerHTML = get(’input’).value;}, 0)
    }

  2. Lunatic Sun Says:

    JavaScript 引擎并不会丢弃事件,在你的例子中

    input.focus();
    input.select();

    已经被执行。并且input不能获取焦点的解决方法不一定是使用setTimeout,在Firefox和Safari中,只要使用return false取消默认行为就能够达到目的。另外Opera不需要任何技巧直接能够正确执行以上两行代码,我想可能是因为Opera没有在button的mousedown事件上设置默认行为。

    不过return false没有解决IE的这个问题,IE确实是执行focus和select方法,input也确实得到了焦点,但是似乎select的默认行为没有被执行,所以IE中看不到input中的文本被选中。

  3. Lunatic Sun Says:

    我刚才说的IE的情况看来是IE的mousedown事件下的一个bug,使用onclick事件就没有这个问题了。

  4. realazy Says:

    @Lunatic Sun 实际上,从一个不是很专业的角度来说,click = mousedown + mouseup. 虽然我没有深入研究,但可以这么假定:mouswdown 时执行创建 dom 事件,而 mouseup 时执行 focus 和 select 的事件,因而没有问题。这也是在追求速度时,推荐使用 mousedown 替换 click 的原因,只不过 mousedown 不像 click 一样,click 时,用户可以不释放鼠标,从而有反悔的机会。

  5. Lunatic Sun Says:

    @realazy - 从用户体验的角度,我推荐使用click事件的原因有两个:

    1 我们应当让用户有反悔的机会;
    2 在button的mousedown事件中使用移开焦点的代码input.focus()会使浏览器本身的绘画button被按下和弹起的那种效果消失。

  6. xiaowei Says:

    应该是这样,input.focus();input.select(); 都执行成功。 但由于采用了onmousedown事件,mousedown后随后触发mouseup于是焦点立即移回到button。这样虽然input.select(); 已执行成功但确看不出来

  7. zamanewby Says:

    我前一段时间在写js代码时, 经常出现在firefox ie7 opera等浏览器工作正常的代码, 在ie6下失效的情况。 后来我发现失效部分的代码用一个setTimeout函数延时一下就可以正常工作了, 我通常都是设1ms的延时, 没试过0, 一直以为是ie6的效率问题。 看过这篇文章比较受启发。 回去试一下, 估计正好能解决因为设了1ms延时对后续部分代码影响的问题了:)

  8. dexter_yy Says:

    ……问题不是阻断罢,而是onkeydown/onkeypress的时候,根本就还没有完成输入罢, value本来就没改变,同样, onmousedown的时候点击事件还未完成,select和focus实际上执行过了,只不过又被点击事件取消了而已……

    你可以把那个测试页面的代码改成这样试试:

    get(’input’).onkeydown = function(){
    get(’preview’).innerHTML += this.value+’1′;
    var me = this;
    setTimeout(function(){
    get(’preview’).innerHTML += me.value+”2″;
    }, 0);
    };
    get(’input’).onkeyup = function(){
    get(’preview’).innerHTML += this.value+’3′;
    var me = this;
    setTimeout(function(){
    get(’preview’).innerHTML += me.value+”4″;
    }, 0);
    };
    get(’input’).onkeypress = function(){
    get(’preview’).innerHTML += this.value+”5″;
    var me = this;
    setTimeout(function(){
    get(’preview’).innerHTML += me.value+”6″;
    }, 0);
    };

  9. realazy Says:

    @dexter_yy 或许我所举的例子不是很好。你所说的 onkeydown/onkeypress的时候,根本就还没有完成输入,我是这样认为的:正是因为 正在输入 这个进程阻断了其他事件,因此才需要 setTimeout 来为被隔断的进程重新排程。

    p.s. 你的 blog 很棒!

  10. Carffuca Says:

    我认为在第3个例子中,我们定义了对onkeypress事件的处理函数(即在span中显示input内的值),而浏览器自身也有一个对onkeypress事件的处理函数(即在input框中显示你输入的那个值)。我认为浏览器把这两个函数放在了一个对onkeypress事件监听的队列里,并且用户定义的函数先运行了,浏览器自己的函数后运行。可以简单的修改一下第3个例子就能看出这个效果。
    get(’input’).onkeypress = function(){
    alert(this.value);
    }
    注意当alert出现的时候input框中并没有值,当点击alert的确定后,input框中出现了输入的值。
    在这里使用setTimeout其实就是推迟了用户定义的那个函数的运行时间,浏览器会在处理好所有onkeypress事件监听函数后运行setTimeout中的内容。

  11. lone Says:

    我之前对click的理解也是click=down+up, 这些实例把down或者press换成up就行了,理由或许正是LZ讲的 追求速度时推荐使用up而不是down,这样同样允许用户反悔而且会保留按钮的动画效果~
    有个实例是 http://www.和www.diandiandian.net的点击效果,赫赫

  12. lone Says:

    1) Javascript不会丢弃事件, 只要在select()语句后再加上一句 alert(’after select’)方法,执行就可以看到此时,input已经被选中;
    2) JS引擎首先执行用户自定义事件处理方法,然后才执行默认行为;
    3) JS引擎碰到setTimeout方法会将其放入队列等待, 并”跳过”其程序块而继续执行其所在方法的后续代码,执行完成之后才从队列中调用setTimeout~
    因此,以下代码同样可以解决问题:
    get(’makeinput’).onmousedown = function(){
    setTimeout(function(){
    var input = document.createElement(’input’);
    input.setAttribute(’type’, ‘text’);
    input.setAttribute(’value’, ‘test1′);
    get(’inpwrapper’).appendChild(input);
    input.focus();
    input.select();
    },0);
    }
    1> 执行onmousedown程序体;
    2> 跳过setTimeout方法,没有其他代码;
    3> 执行setTimeout方法,没有问题;
    ps: lz有必要删除留言么? …………….

    http://lonevan./logs/18841965.html 看您的文章做的些个小实验

  13. 香酥馍馍片 Says:

    realazy 看您的第三个例子。
    可以去尝试的输入几个字符(中文字也试下)
    试的时候:
    1.backspace下 ,delete下,情况还是不同的。
    2.shift ctrl alt等按键。

    可能会有出乎您的预料。
    js里内存是怎么回事,
    貌似不像上边大家所说的那样。

  14. lonevan Says:

    楼上说的应该是在ie6测试的吧(7,8没试过)
    您在firefox下再测试下backspace,delete,shift,ctrl,alt
    顺便再加上 up,down,left,right 等等非字母数字键
    浏览器解释的差异造成的,不过ie可非w3c~ ^^

  15. welcome58 Says:

    楼上诸位都说了很多了,其实input.focus();input.select();都执行了,只是被onmouseup影响了。
    第三个例子,我认为这种效果不应该用onkeypress,因为这个时候还没有完成输入动作,改成onkeyup就不存在延迟了,
    采用setTimeout,确实可以不改动原有的事件,达到效果,理由博主已经说了,“在实践中,setTimeout 会在其完成当前任何延宕事件的事件处理器的执行,以及完成文档当前状态更新后,告诉浏览器去启用 setTimeout 内注册的函数。”

  16. 我de艾蜜莉 Says:

    确实 setTimeout 的问题是存在的,只是 realazy 举的这个例子不太好。
    最近就在 ajax 创建 dom 加载内容的时候遇到了这样的问题,这个问题只在 ie 下存在,firefox 没有问题。

  17. hax Says:

    1. 并不是所有浏览器都支持setTimeout延时为0。而且它们的定时排程也各有差异。

    2. 你的例子其实有问题,所以你后面的论述统统都白搭了,因为你正好碰到了IE的bug(Lunatic Sun的判断是对的,只是这个bug的本质不是那么容易认识到),所以什么问题也证明不了。IE的HTML focus等实际是被转换为windows控件的focus,所以这里存在两种不同层次的focus机制。理想上,HTML的focus应该被同步转换为windows控件的focus,然而IE可怜的代码导致这种转换存在某些不同步的bug。你不幸就遇到了。实际上,即使没有使用setTimeout,调用focus()之后,HTML focus确实已经到了新生成的input中,这一点你可以通过document.activeElement来验证。然而,由于mousedown事件默认会获得控件焦点,所以windows控件focus就跑回了你的按钮上面了(所以实际上这里出现了windows focus机制和html focus机制的脱节)。怎么证明这一点?我过去用过一个调试工具可以显示出每个html控件实际的windows控件,但是一时想不起来那个工具叫什么了。不过此处还会出现一个非常orz的症状可以证明这一点——若干年前我在进行vml编程时发现了这个奇异的bug,这里卖个小关子看看大家能否自己发现它(提示:西方人通常发现不了这个bug)。
    3. 关于是用click还是mousedown/mouseup,这其实就是另外一个话题了。不过mousedown中写代码确实更加容易触发许多非常微妙的IE的bug。
    4. 关于第三个例子,dexter_yy和Carffuca的说法是正确的。这跟排程并没有直接的关系。只不过setTimeout总是会在等到当前执行序列(包括所有同步的事件及相关函数调用)完成之后执行,那个时候value的值已经变化了。

  18. seektan Says:

    不错的文章,看了自己也写了一点总结。 见笑
    http://hi.baidu.com/webworker/blog/item/3ace11d8ac37543333fa1cb0.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多