JavaScript 下的 setTimeout (fn,0) 意味着什么?[转]
近期在研究异步编程的我对于 setTimeout 之类的东西异常敏感。在 SegmentFault 上看到了一个问题《关于 SetTimeout 时间设为 0 时》:提问者读了一篇文章,原文解释 setTimeout 延迟时间为 0 时会发生的事情,提问者提出了几个文章中的几个疑点。读了那篇文章之后发现原文的作者对于 setTimeout 的理解和自己的认知有点出入,于是编写了相关测试的代码以求答案。最终编写了这篇文章。
本文内容如下:
起因
单线程的 JavaScript
setTimeout 背后意味着什么
起因
上午在 SegmentFault 上看到了这个问题《关于 SetTimeout 时间设为 0 时》(注:SegmentFault 正在调整备案,如不能访问,请点击这里),原提问者注明了问题来源:《JS setTimeout 延迟时间为 0 的详解》。这个问题来源也是转载的,我后来找到了出处。
在问题来源的那篇的文章中(后者),讲述了 JS 是单线程引擎:它把任务放到队列中,不会同步去执行,必须在完成一个任务后才开始另外一个任务。
而后,转载的那篇文章列出并补充了原文的栗子:
setTimeout
1、未使用 setTimeout
<h2>2、使用 <code>setTimeout</code></h2>
<button id="makeinput2">生成 input</button>
<p id="inpwrapper2"></p>
<h2>3、另一个例子</h2>
<p>
<input type="text" id="input" value="" /><span id="preview"></span>
</p>
代码运行实例请戳这里。
原文中有这么一段话,描述的有点抽象:
JavaScript 引擎在执行 onmousedown 时,由于没有多线程的同步执行,不可能同时去处理刚创建元素的 focus 和 select 方法,由于这两个方法都不在队列中,在完成 onmousedown 后,JavaScript 引擎已经丢弃了这两个任务,正如第一种情况。而在第二种情况中,由于 setTimeout 可以把任务从某个队列中跳脱成为新队列,因而能够得到期望的结果。
我看到这里就觉得非常不对劲了。因为按照这种任务会被丢弃的说法,那么只要在事件触发的函数中再触发其他的事件都会被丢弃,浏览器是绝对不会这么做的,于是我编写了测试代码:
window.onload = function () {
//第一个例子:未使用setTimeout
get('makeinput').onmousedown = function () {
var input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('value', 'test1');
get('inpwrapper').appendChild(input);
//按照文中的理论,这里的click不会被触发,但它却成功触发了
get('inpwrapper').click();//触发了inpwrapper的onclick事件
}
get('inpwrapper').onclick = function () {
alert('linkFly');
};
}
下面的 onclick () 最终是执行了:弹出了”linkFly”。
而在转载的文中为了引人深思,又提出了第三个例子:
在此,你可以看看例子 3,它的任务是实时更新输入的文本,现在请试试,你会发现预览区域总是落后一拍,比如你输 a, 预览区并没有出现 a, 在紧接输入 b 时,a 才不慌不忙地出现。
而文中最后留给大家的思考的问题,解决方案就是使用 setTimeout 再次调整浏览器的代码任务运行队列。
var domInput = get('input');
domInput.onkeypress = function () {
setTimeout(function () {
//第三个例子的问题就这样就会被解决
get('preview').innerHTML = domInput.value;
})
}
原文和转载的文章中都对 setTimeout (fn,0) 进行了思考,但原文指出的问题本质漏洞百出,所以才出了这篇文章,我们的正文,现在开始。
单线程的 JavaScript
首先我们来看浏览器下的 JavaScript:
- javascriipt 引擎线程
- GUI 渲染线程
- 浏览器事件触发线程
javascript 引擎是基于事件驱动单线程执行的,JS 引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个 JS 线程在运行 JS 程序。
GUI 渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流 (reflow) 时,该线程就会执行。但需要注意 GUI 渲染线程与 JS 引擎是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可来自 JavaScript 引擎当前执行的代码块如 setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理。(当线程中没有执行任何同步代码的前提下才会执行异步代码)
js 的单线程在这一段面试代码中尤为明显(理解即可,请不要尝试… 浏览器会假死的):
var isEnd = true;
window.setTimeout(function () {
isEnd = false;//1s后,改变isEnd的值
}, 1000);
//这个while永远的占用了js线程,所以setTimeout里面的函数永远不会执行
while (isEnd);
//alert也永远不会弹出
alert('end');
在我工作中对 js 的认识,个人认为 js 的任务单位是函数。即,一个函数表示着一个任务,这个函数没有执行结束,则在浏览器中当前的任务即没有结束。
上面的代码中,当前任务因为 while 的执行而造成永远无法执行,所以后面的 setTimeout 也永远不会被执行。它在浏览器的任务队列中如图所示:
Browser Event
setTimeout 背后意味着什么
这篇文章一直在使用 setTimeout 为我们展现和理解 js 单线程的设计,只是它错误的使用了 Event 来进行演示,并过度解读了 Event。
这里原文和转载的文章忽略了这些基础的事件触发,而且也偏偏挑了两套本身设计就比较复杂的 API:onmouseXXX 系和 onkeyXXX 系。
onKeyXXX 系的 API 触发顺序如图:
onKeyXXX
而我个人所理解它们对应的功能:
onkeydown - 主要获取和处理当前按下按键,例如按下 Enter 后进行提交。在这一层,并没有更新相关 DOM 元素的值。
onkeypress - 主要获取和处理长按键,因为 onkeypress 在长按键盘的情况下会反复触发直到释放,这里并没有更新相关 DOM 元素的值,值得注意的是:keypress 之后才会更新值,所以在长按键盘反复触发 onkeypress 事件的时候,后一个触发的 onkeypress 能得到上一个 onkeypress 的值。所以出现了 onkeypress 每次取值都会是上一次的值而不是最新值。
onkeyup - 触发 onkeyup 的 DOM 元素的值在这里已经更新,可以拿到最新的值,所以这里主要处理相关 DOM 元素的值。
流程就是上面的图画的那样:
onkeydown => onkeypress => onkeyup
使用了 setTimeout 之后,流程应该是下面这样子的:
onkeydown => onkeypress => function => onkeyup
使用 setTimeout (fn,0) 之后,在 onkeypress 后面插入了我们的函数 function。上面所说,浏览器在 onkeypress 之后就会更新相关 DOM 元素的状态(input [type=text] 的 value),所以我们的 function 里面可以拿到最新的值。
所以我们在 onkeypress 里面挂起 setTimeout 能拿到正确的值,下面的代码可以测试使用 setTimeout (fn,0) 之后的流程:
window.onload = function () {
var domInput = get('input'), view = get('preview');
//onkeypress兼容性和说明:http://www.w3school.com.cn/jsref/jsref_events.asp
domInput.onkeypress = function () {
setTimeout(function () {
//这个函数在keypress之后,keyup之前执行
console.log('linkFly');
});
};
domInput.onkeyup = function () {
console.log('up');
};
};
然后我们再来谈谈原代码中的示例 1 和示例 2,示例 1 和示例 2 的区别在这里:
//示例1
input.focus();
input.select();
//示例2
setTimeout(function () {
input.focus();
input.select();
}, 0);
原文章中说示例 1 的 focus () 和 select () 在 onmousedown 事件中被丢弃,从而导致了没有选中,但原文的作者忽略了他注册的事件是:onmousedown。
我们暂且不讨论 onmouseXXX 系的其他 API,我们仅关注和点击相关的,它们的执行顺序是:
mousedown - 鼠标按钮按下
mouseup - 鼠标按钮释放
click - 完成单击
我们在 onmousedown 里面新建了 input,并且选中 input 的值(调用了 input.focus (),input.select ())。
那么为什么没有被选中呢?这样,我们来做一次测试,看看我们的 onfocus 到底是被丢弃了,还是触发了。我们把原文的代码进行改写:
window.onload = function () {
var makeBtn = get('makeinput');
//观察onmouseXXX系完成整个单击的顺序
makeBtn.onmousedown = function (e) {
console.log(e.type);
var input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('value', 'test1');
get('inpwrapper').appendChild(input);
input.onfocus = function () {//观察我们新生成的input什么时候获取焦点的,或者它有没有像原文作者说的那样被丢弃了
console.info('input focus');
};
input.focus();
input.select();
}
makeBtn.onclick = function (e) {
console.log(e.type);
};
makeBtn.onmouseup = function (e) {
console.log(e.type);
};
makeBtn.onfocus = function () {//观察我们生成按钮什么时候获取焦点的
console.log('makeBtn focus');
}
};
代码运行的结果是这样的:
onmouseXXX & focus
我们的 input focus 执行了 —— 那么它为什么没有获取到焦点呢?我们再看看后面执行的函数:我们点击的按钮,在 mousedown 之后,才获得焦点,也就是说:我们的 input 本来已经得到了 focus (),但在 onmousedown 之后,我们点击的按钮才迟迟触发了自己的 onfocus (),导致我们的 input 被覆盖。
我们再加上 setTimeout 进行测试:
window.onload = function () {
var makeBtn = get('makeinput');
makeBtn.onmousedown = function (e) {
console.log(e.type);
var input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('value', 'test1');
get('inpwrapper').appendChild(input);
input.onfocus = function () {
console.info('input focus');
};
//我们加上setTimeout,看看会发生什么
setTimeout(function () {
input.focus();
input.select();
});
}
makeBtn.onclick = function (e) {
console.log(e.type);
};
makeBtn.onmouseup = function (e) {
console.log(e.type);
};
makeBtn.onfocus = function () {
console.log('makeBtn focus');
}
};
执行结果是这样:
onmouseXXX and settimeout
可以看见当我们点击” 生成” 按钮的时候,按钮的 focus 正确的执行了,然后才执行了 input focus。
在示例 1 中,我们在 onmousedown () 中执行了 input.focus () 导致 input 得到焦点,而 onmousedown 之后,我们点击的按钮才迟迟得到了自己的焦点,造成了我们 input 刚拿到手还没焐热的焦点被转移。
而示例 2 中的代码,我们延迟了焦点,当按钮获得焦点之后,我们的 input 再把焦点抢过来,所以,使用 setTimeout (fn,0) 之后,我们的 input 可以得到焦点并选中文本。
这里值得思考的 focus () 的执行时机,根据这次测试观察,发现 focus 事件好像挂载在 mousedown 之内的最后面,而不是直接挂在 mousedown 的后面。它和 mousedown 仿佛是一体的。
我们使用 setTimeout 之前的任务流程是这样的(-> 表示在上一个任务中,=> 表示在上一个任务后):
onmousedown -> onmousedown中执行了input.focus() -> button.onfocus => onmouseup => onclick
onmouseXXX 事件流程
而我们使用了 setTimeout 之后的任务流程是这样的:
onmousedown -> button.onfocus => input.focus => onmouseup => onclick
onmouseXXX+setTimeout 事件流程
而从上面的流程上我们得知了另外的消息,我们还可以把 input.focus 挂在 mouseup 和 click 下,因为在这些事件之前,我们的按钮已经得到过焦点了,不会再抢我们的焦点了。
makeBtn.click = function (e) {
console.log(e.type);
var input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('value', 'test1');
get('inpwrapper').appendChild(input);
input.onfocus = function () {//观察我们新生成的input什么时候获取焦点的
console.info('input focus');
};
input.focus();
input.select();
}
我们应该认识到,利用 setTimeout (fn,0) 的特性,可以帮助我们在某些极端场景下,修正浏览器的下一个任务
总结:
意思就是说 input 的 focus 有执行, 只是被 button 的 mouseup 影响了,所以要要把 focus () 放到后面执行
页面上只有一个元素能获得焦点,就这样而已。。
参考和引用
JavaScript 异步机制
什么是 Event Loop
javascript 线程解释
JavaScript - 前端开发交流群:377786580
作者:linkFly
原文:http://www.cnblogs.com/silin6/p/4333999.html
出处:www.cnblogs.com/silin6/