JavaScript下的setTimeout(fn,0)意味着什么?

近期在研究异步编程的我对于setTimeout之类的东西异常敏感。在SegmentFault上看到了一个问题《关于SetTimeout时间设为0时》:提问者读了一篇文章,原文解释setTimeout延迟时间为0时会发生的事情,提问者提出了几个文章中的几个疑点。读了那篇文章以后发现原文的做者对于setTimeout的理解和本身的认知有点出入,因而编写了相关测试的代码以求答案。最终编写了这篇文章。 javascript

本文内容以下:html

  • 原由
  • 单线程的JavaScript
  • setTimeout背后意味着什么
  • 参考和引用

JavaScript - 前端开发交流群:377786580前端

原由

上午在SegmentFault上看到了这个问题《关于SetTimeout 时间设为0时》(注:SegmentFault正在调整备案,如不能访问,请点击这里),原提问者注明了问题来源:《JS setTimeout延迟时间为0的详解》。这个问题来源也是转载的,我后来找到了出处
在问题来源的那篇的文章中(后者),讲述了JS是单线程引擎:它把任务放到队列中,不会同步去执行,必须在完成一个任务后才开始另一个任务。
然后,转载的那篇文章列出并补充了原文的栗子:java

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>setTimeout</title>
    <script type="text/javascript">
        function get(id) {
            return document.getElementById(id);
        }
        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);
                input.focus();
                input.select();
            }
            //第二个例子:使用setTimeout
            get('makeinput2').onmousedown = function () {
                var input = document.createElement('input');
                input.setAttribute('type', 'text');
                input.setAttribute('value', 'test1');
                get('inpwrapper2').appendChild(input);
                //setTimeout
                setTimeout(function () {
                    input.focus();
                    input.select();
                }, 0);
            }
            //第三个例子,onkeypress输入的时候少了一个值
            get('input').onkeypress = function () {
                get('preview').innerHTML = this.value;
            }
        }
    </script>
</head>
<body>
    <h1><code>setTimeout</code></h1>
    <h2>一、未使用 <code>setTimeout</code></h2>
    <button id="makeinput">生成 input</button>
    <p id="inpwrapper"></p>


    <h2>二、使用 <code>setTimeout</code></h2>
    <button id="makeinput2">生成 input</button>
    <p id="inpwrapper2"></p>


    <h2>三、另外一个例子</h2>
    <p>
        <input type="text" id="input" value="" /><span id="preview"></span>
    </p>
</body>
</html>

代码运行实例请戳这里
原文中有这么一段话,描述的有点抽象:面试

JavaScript引擎在执行onmousedown时,因为没有多线程的同步执行,不可能同时去处理刚建立元素的focus 和select方法,因为这两个方法都不在队列中,在完成onmousedown后,JavaScript 引擎已经丢弃了这两个任务,正如第一种状况。而在第二种状况中,因为setTimeout能够把任务从某个队列中跳脱成为新队列,于是可以获得指望的结果。编程

我看到这里就以为很是不对劲了。由于按照这种任务会被丢弃的说法,那么只要在事件触发的函数中再触发其余的事件都会被丢弃,浏览器是绝对不会这么作的,因而我编写了测试代码:segmentfault

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才镇定自若地出现。app

而文中最后留给你们的思考的问题,解决方案就是使用setTimeout再次调整浏览器的代码任务运行队列。

var domInput = get('input');
    domInput.onkeypress = function () {
        setTimeout(function () {
            //第三个例子的问题就这样就会被解决
            get('preview').innerHTML = domInput.value;
        })
    }

原文和转载的文章中都对setTimeout(fn,0)进行了思考,但原文指出的问题本质漏洞百出,因此才出了这篇文章,咱们的正文,如今开始。

单线程的JavaScript

首先咱们来看浏览器下的JavaScript:
浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:javascript引擎线程,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)的特性,能够帮助咱们在某些极端场景下,修正浏览器的下一个任务。

到了这里,咱们已经能够否认原文所说的:"JavaScript引擎已经丢弃了这两个任务"。
我仍然相信,浏览器是爱咱们的(除了IE6和移动端一些XXOO的浏览器!!!!)浏览器并不会无缘无故的丢弃咱们辛劳写下的代码,多数时候,只是由于咱们没有看见背后的真相而已。

当咱们踏进计算机的世界写下"hello world"的时候就应该坚信,这个二进制的世界里,永远存在真相。

参考和引用

JavaScript - 前端开发交流群:377786580

做者:linkFly
声明:嘿!你都拷走上面那么一大段了,我以为你应该也不介意顺便拷走这一小段,但愿你可以在每一次的引用中都保留这一段声明,尊重做者的辛勤劳动成果,本文与博客园共享。
相关文章
相关标签/搜索