• JavaScript定时器及其他


    Jun 20 2014 Updated:Jun 20 2014

    平时工作中不可避免地要嵌套网页,对JavaScript的深入了解还是很有必要滴。而JavaScript中一个容易让人迷惑的地方就是定时器了,恐怕我们每天都在用,但我们真的足够理解吗?反正我之前只是随便用用,最近拜读了一些资料,感觉还是收获不少,在此作一个归纳。

    最重要的概念

    JavaScript引擎是单线程的。HTML5引入了Web Workers的特性,会从一定程度上突破这个限制。但话说回来,我们还是需要面对现实,认清国情。

    单线程意味着,JavaScript代码并不会像线程一样被切来切去,更不会有两段代码同时执行,在同一个时刻始终只能有一段代码得到执行,各段代码依次执行下去。我们脑海中要时刻有这个概念。

    定时器怎么创建

    定时器并非JavaScript语言本身的特性,而是浏览器提供给我们的一个特性,它的作用是允许我们异步地将一段代码推迟一定毫秒后执行。

    先看怎么用,具体来说,浏览器给了我们两个全局的方法去创建定时器:

    • id = setTimeout(fn, delay) 初始化一个定时器,它将在指定的delay毫秒后执行传入的fn回调函数。返回一个唯一标识这个定时器的值。
    • id = setInterval(fn, delay) 初始化一个定时器,每隔deplay毫秒就执行传入的fn回调函数。返回一个唯一标识这个定时器的值。

    相应的有两个取消定时器的方法:clearTimeout(id)和clearInterval(id),作用是一样的,都是取消标识为id的定时器,事实上用任意一个都行,不过一般和创建定时器的方法相对应以使代码清晰易读。

    定时器如何工作

    单看方法的描述,我们很容易误解,天真地以为就是它说的那样。这个时候我们就不要忘记最重要的概念:单线程。事实上,单线程环境中的定时器可能一点也不那么定时!

    由于单线程的限制,我们用来处理异步事件(定时器)的回调函数,只有当没有其他代码在执行时才会得到执行。因此,这些事件处理函数(定时器回调)会排队执行,当其中一个执行时,另外一个必须等着。JQuery之父有一篇博客很好地描述了定时器的机制,其中 有一幅图非常经典,我在这里把它贴出来并解释一下。

    定时器工作示意图

    从上往下表示的是时间的流逝,蓝色的方框表示的是一段段代码。第一段代码(可以看成我们的主要逻辑)中创建了一个10ms的setTimeout和一个10ms的setInterval,在第一段代码里面还未执行完时,10ms的setTimeout就触发了,但此时它的处理函数并不会立即被执行(单线程),而是加到了队列中等待时机。在setTimeout触发之前还有一个鼠标单击事件触发,像鼠标单击事件这样的界面事件也是异步处理的,所以它也不会被立即执行,同样被放到了队列中。

    等第一个代码段执行完毕后,现在cpu空闲了,于是问,有谁还在排队等待执行的吗?这时我们知道队列中有两个事件处理函数等着,一般情况下,浏览器会采取先进先出的原则取出最先入队的函数运行(本例中假设如此,要注意这不是确定的,浏览器也可能有自己的特定算法),于是鼠标点击事件响应函数得到执行,在它执行的过程中setInterval的第一次触发,但cpu说,不好意思,我很忙,你先排着吧。

    到鼠标点击事件处理(第二段代码)完毕后,队列中还剩早就触发却还在苦逼地等着的setTimeout的回调函数以及新加入的setInterval的回调函数。又一次闲下来的cpu于是取出setTimeout的函数开始执行(泪流满面),还没执行多久,setInterval第二次触发了,但悲剧的是,第一次触发都还没执行!是不是又继续排队呢?浏览器会说,队列中已经有了一个在等待了,你这同一个setInterval就别再掺和了,于是,直接忽略这次触发!等setTimeout处理完后,终于轮到了setInterval的第一次触发执行了,在它快要执行完时(40ms),setInterval第三次触发,这时由于队列中已经没有同一个setInterval等着,因此会正常进入队列等待。

    后面到setInterval的第三次触发响应执行完后,队列空了,于是暂时就没啥 事干了,等到第四次触发时由于没有人在排队,于是响应函数马上得以执行,这次总算准了一回!

    总之,由于JavaScript是单线程的,我们永远无法确定我们设的定时器会按照我们指定的延迟时间准确地执行。

    setTimeout和setInterval的区别

    setInterval从功能上很像是一个setTimeout周期性地重复调用自己,但它们并不等同,存在着微妙的差别。

    比如下面的两段类似的代码:

    setTimeout(function repeatMe() {
        /* 一段代码... */
        setTimeout(repeatMe, 10);
    }, 10);
    
    setInterval(function() {
        /* 一段代码... */
    }, 10);
    

    你可能会觉得这两种方式从功能上等价,事实上不然。

    • setTimeout保证的是下一次回调执行至少在这次回调执行完的10ms以后,也就是说不少于10ms。
    • setInterval则会每隔10ms去尝试执行一次回调,不管上一次回调什么时候执行完(但setInterval的每一次尝试并不一定会成功,也就是说同一个setInterval的两次触发不会都被排队)

    异步事件

    浏览器是事件驱动的。在浏览器看来,绝大部分动作都是异步的,这些动作只会触发一个事件并加到队列中。

    除了上面详细描述的定时器外,还包括鼠标移动、窗口大小调整等动作都会触发一个异步事件。

    浏览器内部有一个事件循环,它的责任就是检查队列和处理事件。事实上绝大多数交互和活动都会通过事件循环来运行。有时候一个简单的动作可能会导致一系列事件加入队列中,比如单击鼠标还会伴随触发鼠标按下和鼠标释放两个动作。这跟桌面GUI程序的原理是一致的。

    setTimeout(func, 0)有啥用

    我们经常会看到把setTimeout的第2个参数即延迟设为0,这不是说马上执行吗?那为何还要用setTimeout呢?看了先前对setTimeout工作机制的描述后,应该会明白,这样是告诉浏览器,尽快执行我要的动作,但尽快并不等于立即!还是得放到队列中,最快也要等浏览器的内部循环的下一个周期来到时才能执行。

    这种用法是一个很好的技巧,我自己就用过下面的这个经典例子,目的是想使一个input中输入的字母强制转换成大写。乍一看,我们会不太费力地想到如下实现:

    document.getElementById('myInput').onkeypress = function(event) {
        this.value = this.value.toUpperCase();
    }
    

    我开始也是这样写的,但事与愿违,输入框中的最后一个字母没变成大写。原因在于浏览器会在keypress事件处理后才会将输入的字符加到输入框中。这就是setTimeout大显身手的时候了:

    document.getElementById('myInput').onkeypress = function(event) {
        var that = this;
        setTimeout(function() {
            that.value = that.value.toUpperCase();
        }, 0);
    }
    

    这样就会发现问题解决了。是不是很神奇?这样用实际上是为了让浏览器将字符先加到输入框中,然后才执行我们想要的转换大小写的动作,当然这个间隔足够短,As Soon As Possible!

    另外它也常用来延迟子元素上的事件触发,让父元素的事件处理先起作用。由于事件响应是自下向上冒泡的,通常会先处理子元素的事件,如果我们要反过来呢?看下面的代码:

    var input = document.getElementsByTagName('input')[0];
    input.onclick = function() {
        setTimeout(function() {
            input.value +=' input'
        }, 0);
    };
    document.body.onclick = function() {
        input.value += ' body';
    };
    

    通过在input这个子元素的click事件处理函数中用这个技术,我们就可以让body父元素的先响应click事件。

    定时器、界面渲染、耗时任务

    对于大多数浏览器来说,JavaScript的执行和浏览器页面渲染共用同一个线程,这意味着,在JavaScript执行的时候会阻塞界面的渲染。所以如果我们在JavaScript中做长时间的计算密集型的操作,会导致界面失去响应,这对用户来说极不友好。另外,一些浏览器直接会警告用户你的脚本失去响应,甚至强行终止掉你的脚本。所以我们要避免一次执行一个过于耗时的任务,如果有这种任务,我们可以通过定时器来对任务进行拆分,以腾出机会给到界面渲染,使界面处于可交互的状态。

    比如类似这样的对DOM进行大量更改的操作:

    var tbody = document.getElementsByTagName("tbody")[0];
    for (var i = 0; i < 20000; i++) {
        var tr = document.createElement("tr");
        for (var t = 0; t < 6; t++) {
            var td = document.createElement("td");
            td.appendChild(document.createTextNode(i + "," + t));
            tr.appendChild(td);
        }
        tbody.appendChild(tr);
    }
    

    上面的代码可能会非常耗时,而且界面的渲染不可同时进行,意味着你这段代码执行完毕之前,页面不会有任何动静,所以在这种情况下,就应该利用好定时器,一次做一部分,然后让出执行权,让界面逐步渲染。

    后记

  • 相关阅读:
    WIN10解决:失败 – 检测到病毒文件下载失败问题
    Jinja2学习
    div设置百分比高度 宽度
    flask静态html
    python排序之冒泡排序
    python中的break continue之用法
    python中的break continue用法
    python格式化输出(% format用法)
    python基础数据类型
    linux下anaconda使用教程
  • 原文地址:https://www.cnblogs.com/libinsoft/p/4536056.html
Copyright © 2020-2023  润新知