根据HTML 5标准,setTimeout推迟执行的时间,最少是5毫秒。如果小于这个值,会被自动增加到5ms。

每一个setTimeout在执行时,会返回一个唯一ID,把该ID保存在一个变量中,并传入clearTimeout,可以清除定时器。

在setTimeout内部,this绑定采用默认绑定规则,也就是说,在非严格模式下,this会指向window;而在严格模式下,this指向undefined。

setTimeout不止有2个参数,第一个参数是回调函数,第二个参数是时间,第三个参数以后都是第一个回调函数的参数。

一、用setTimeout代替setInterval

由于setInterval间歇调用定时器会因为在定时器代码未执行完毕时又向任务队列中添加定时器代码,导致某些间隔被跳过等问题,所以应使用setTimeout代替setInterval。

setTimeout(function myTimer() {
  /**
   * 需要执行的代码
   * setTimeout会等到定时器代码执行完毕后才会重新调用自身(递归),记得给匿名函数添加一个函数名,以便调用自身。
   */
  setTimeout(myTimer, 1000);
}, 1000);

这样做的好处是,在前一个定时器执行完毕之前,不会向任务队列中插入新的定时器代码,可以避免任何缺失的间隔,还可以保证在下一次定时器代码执行前,至少要等待指定的间隔,避免了连续执行。这个模式主要用于重复定时器。

// 代码段1,间歇性输出1到10
let num = 0;
let max = 10;
setTimeout(function myTimer() {
  num++;
  console.log(num);
  if (num === max) {
      return;
  }
  setTimeout(myTimer, 500);
}, 500);
// 代码段2,间歇性输出1到10
setTimeout(function myTimer() {
  num++;
  console.log(num);
  if (num < max) {
      setTimeout(myTimer, 500);
  }
}, 500);

二、在for循环中创建setTimeout定时器

1、根据事件循环和任务队列的原理,定时器通常在循环结束后才会加入到任务队列执行。

2、定时器是循环创建的。

3、定时器几乎是同时开始计时的。

4、定时器中的回调函数属于闭包,包含着对循环后全局变量i的引用。在块作用域和定时器外创建一个函数作用域时,此时不会查找全局作用域。

5、定时器的第二个参数不属于闭包的一部分,其值与循环i的值相同。

程序运行遵循同步优先异步靠边回调垫底

// 代码段1,输出6个5
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
      console.log(i);
  }, 1000 * i);
}
console.log(i);

第1个5直接输出,1 秒之后,输出 5 个 5,并且每隔1s输出一个,一共用时4s。

for循环和循环体外部的console是同步的,所以先执行for循环,再执行外部的console.log。等for循环执行完,就会给setTimeout传参,最后执行。

JavaScript单线程如何处理回调呢?JavaScript同步的代码是在堆栈中顺序执行的,而setTimeout回调会先放到消息队列,for循环每执行一次,就会放一个setTimeout到消息队列排队等候,当同步的代码执行完了,再去调用消息队列的回调方法。这个消息队列执行的时间,需要等待到函数调用栈清空之后才开始执行。即所有可执行代码执行完毕之后,才会开始执行由setTimeout定义的操作。而这些操作进入队列的顺序,则由设定的延迟时间来决定,消息队列遵循先进先出(FIFO)原则。因此,即使我们将延迟时间设置为0,它定义的操作仍然需要等待所有代码执行完毕后才开始执行。这里的延迟时间,并非相对于setTimeout执行这一刻,而是相对于其他代码执行完毕这一刻。

先执行for循环,按顺序放了5个setTimeout回调到消息队列,然后for循环结束,下面还有一个同步的console,执行完console之后,堆栈中已经没有同步的代码了,就去消息队列找,发现找到了5个setTimeout,注意setTimeout是有顺序的。

JavaScript在把setTimeout放到消息队列的过程中,循环的i是不会及时保存进去的,相当于你写了一个异步的方法,但是ajax的结果还没返回,只能等到返回之后才能传参到异步函数中。

for循环结束之后,因为i是用var定义的,所以var是全局变量(这里没有函数,如果有就是函数内部的变量),这个时候的i是5,从外部的console输出结果就可以知道。那么当执行setTimeout的时候,由于全局变量的i已经是5了,所以传入setTimeout中的每个参数都是5。很多人都会以为setTimeout里面的i是for循环过程中的i,这种理解是不对的。

for (var i = 0; i < 5; i++) {
  console.log(i);
  setTimeout(function myTimer() {
      console.log(i);
  }, i * 1000);
}

立刻输出0 1 2 3 4

间歇性输出5个5

温馨提示:如果在开发者工具console面板运行这段程序,你会看到不一样的结果。
立刻输出0 1 2 3 4
立即输出定时器ID
间歇性输出5个5
for (var i = 0; i < 5; i++) {
  setTimeout((function() {
    console.log(i);
  })(), 1000 * i);
}

立即输出0 1 2 3 4。因为setTimeout的第一个参数是函数或者字符串,而此时函数又立即执行了。因此,定时器失效,直接输出0 1 2 3 4。

for (var i = 0; i < 5; i++) {
  (function() {
    console.log(i);
  })();
}

该程序也是立即输出0 1 2 3 4。

三、如何让程序间歇性输出0 1 2 3 4呢?

这里有两种思路,不过原理都相同。

思路1:ES6 let关键字,给setTimeout定时器外层创建一个块作用域。

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

思路1的另一种表达

for (var i = 0; i < 5; i++) {
  let j = i;  //闭包的块作用域
  setTimeout(function() {
    console.log(j);
  }, 1000 * j);
}

思路2:IIFE,创建函数作用域以形成闭包。

Immediately Invoked Function Expression:声明即执行的函数表达式。

for (var i = 0; i < 5; i++) {
  (function iife(j) {     //闭包的函数作用域
    setTimeout(function() {
        console.log(j);
    }, 1000 * i);   //这里将i换为j, 可以证明以上的想法。
  })(i);
}

给定时器外层创建了一个IIFE,并且传入变量i。此时,setTimeout会形成一个闭包,记住并且可以访问所在的词法作用域。因此,会间歇输出0 1 2 3 4。

实际上,函数参数,就相当于函数内部定义的局部变量,因此下面的写法也是可以的,思路2的另一种表达。

for (var i = 0; i < 5; i++) {
  (function iife() {
    var j = i;
    setTimeout(function() {
      console.log(j);
    }, 1000 * i);   //如果这里将i换为j, 可以证明以上的想法。
  })();
}

思路3

for (var i = 0; i < 5; i++) {
  setTimeout(function(j) {
    return function(){
        console.log('index is ',j);
    }
  }(i), 1000 * i);   //如果这里将i换为j, 可以证明以上的想法。
}

思路4

var myTimer = function (i) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
};
for (var i = 0; i < 5; i++) {
  myTimer(i);  //这里传过去的i值被复制了
}
console.log(i);

代码执行时,立即输出5,之后每隔1秒依次立刻输出0 1 2 3 4。

四、如何让程序间歇性输出0 1 2 3 4 5呢?

思路1

for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
       console.log( j);
    }, 1000 * j);  //这里修改0~4的定时器时间
  })(i);
}
setTimeout(function() { //这里增加定时器,超时设置为5秒
  console.log(i);
}, 1000 * i);

我们都知道使用Promise处理异步代码比回调机制让代码可读性更高,但是使用Promise的问题也很明显,即如果没有处理Promise的reject,会导致错误被丢进黑洞,好在新版的Chrome和Node 7.x 能对未处理的异常给出Unhandled Rejection Warning,而排查这些错误还需要一些特别的技巧(浏览器、Node.js)

思路2

const myArr = [];
for (var i = 0; i < 5; i++) {   // 这里i的声明不能改成let,如果要改该怎么做?
  ((j) => {
    myArr.push(new Promise((resolve) => {
      setTimeout(() => {
        console.log(new Date, j);
        resolve();  //这里一定要resolve,否则代码不会按预期执行
      }, 1000 * j); //定时器的超时时间逐步增加
    }));
  })(i);
}

Promise.all(myArr).then(() => {
  setTimeout(() => {
    console.log(new Date, i);
  }, 1000);   // 注意这里只需要把超时设置为1秒
});

思路3

const myArr = []; //这里存放异步操作的Promise
const myTimer = (i) => new Promise((resolve) => {
  setTimeout(() => {
    console.log(new Date, i);
    resolve();
  }, 1000 * i);
});
// 生成全部的异步操作
for (var i = 0; i < 5; i++) {
  myArr.push(myTimer(i));
}
// 异步操作完成之后,输出最后的 i
Promise.all(myArr).then(() => {
  setTimeout(() => {
    console.log(new Date, i);
  }, 1000);
});

思路4:使用ES7中的async await特性

// 模拟其他语言中的sleep,实际上可以是任何异步操作。
const sleep = (timeountMS) => new Promise((resolve) => {
  setTimeout(resolve, timeountMS);
});
(async () => {  //声明即执行的async函数表达式
  for (var i = 0; i < 5; i++) {
    await sleep(1000);
    console.log(new Date, i);
  }

  await sleep(1000);
  console.log(new Date, i);
})();

五、清除定时器

function fn1(){
    for(var i = 0;i < 5; i++){
        var tc = setTimeout(function(i){
            console.log(i);
            clearTimeout(tc);
        },10,i);
    }
}
fn1();//0 1 2 3

解读fn1,这个tc是定义在闭包外面的,也就是说tc并没有被闭包保存,所以这里的tc指的是最后一个循环留下来的tc,所以最后一个4被清除了,没有输出。

function fn2(){
    for(var i = 0;i < 5; i++){
        var tc = setInterval(function(i,tc){
            console.log(i);
            clearInterval(tc);
        },10,i,tc);
    }
}
fn2();//0 1 2 3 4 4 4 4

解读fn2,可以发现最后一个定时器没被删除。在浏览器中单步调试,在第一次循环的时候tc并没有被赋值,所以是undefined,在第二次循环的时候,定时器其实清理的是上一个循环的定时器。所以导致每次循环都是清理上一次的定时器,而最后一次循环的定时器没被清理,导致一直输出4。

六、阅读下列程序,说出运行结果顺序。

let a = new Promise(
  function(resolve, reject) {
    console.log(1);
    setTimeout(() => console.log(2), 0);
    console.log(3);
    console.log(4);
    resolve(true);
  }
);
a.then(v => {
  console.log(8);
});
let b = new Promise(
  function() {
    console.log(5);
    setTimeout(() => console.log(6), 0);
  }
)
console.log(7);

输出结果:1 3 4 5 7 8 2 6。

程序结果分析如下:

1、a变量是一个Promise,Promise本身是同步的,Promise的then()和catch()方法是异步的,所以这里先执行a变量内部的Promise同步代码,输出1 3 4。(同步优先)至于setTimeout回调,先去消息队列排队等着吧。(回调垫底)执行resolve(true),进入then(),then是异步,下面还有同步没执行呢,所以then也去消息队列排队等候吧。(异步靠边)

2、b变量也是一个Promise,和a一样,执行内部的同步代码,输出5,setTimeout滚去消息队列排队等候。

3、最下面同步输出7。

4、同步的代码执行完了,JavaScript就跑去消息队列呼叫异步的代码。这里只有一个异步then,所以输出8。

5、异步执行结束,终于轮到回调啦。这里有2个回调在排队,他们的时间都设置为0,所以不受时间影响,只跟排队先后顺序有关。这时,先输出a里面的回调2,最后输出b里面的回调6。

我们还可以稍微做一点修改,把a里面Promise的 setTimeout(() => console.log(2), 0)改成 setTimeout(() => console.log(2), 2),对,时间改成了2ms,为什么不改成1试试呢?1ms的话,浏览器都还没有反应过来呢。你改成大于或等于2的数字就能看到2个setTimeout的输出顺序发生了变化。所以回调函数正常情况下是在消息队列顺序执行的,但是使用setTimeout的时候,还需要注意时间的大小也会改变它的顺序。

闭包应用之延迟函数setTimeout的更多相关文章

  1. Javascript 闭包与高阶函数 ( 二 )

    在上一篇 Javascript 闭包与高阶函数 ( 一 )中介绍了两个闭包的作用. 两位大佬留言指点,下来我会再研究闭包的实现原理和Javascript 函数式编程 . 今天接到头条 HR 的邮件,真 ...

  2. [Node.js] 闭包和高阶函数

    原文地址:http://www.moye.me/2014/12/29/closure_higher-order-function/ 引子 最近发现一个问题:一部分写JS的人,其实对于函数式编程的概念并 ...

  3. Javascript闭包和C#匿名函数对比分析

    C#中引入匿名函数,多少都是受到Javascript的闭包语法和面向函数编程语言的影响.人们发现,在表达式中直接编写函数代码是一种普遍存在的需求,这种语法将比那种必须在某个特定地方定义函数的方式灵活和 ...

  4. Delphi 延迟函数 比sleep 要好的多

    转自:http://www.cnblogs.com/Bung/archive/2011/05/17/2048867.html //延迟函数:方法一 procedure delay(msecs:inte ...

  5. JavaScript之闭包与高阶函数(一)

    JavaScript虽是一门面向对象的编程语言,但同时也有许多函数式编程的特性,如Lambda表达式,闭包,高阶函数等. 函数式编程是种编程范式,它将电脑运算视为函数的计算.函数编程语言最重要的基础是 ...

  6. 延迟函数 比sleep效果好

    sleep是会阻塞线程的 网上有些延迟函数测试下来还是会阻塞,而接下来推荐的代码则不会   1 2 3 4 5 6 7 8 9 procedure delay(dwMilliseconds:integ ...

  7. Javascript 闭包与高阶函数 ( 一 )

    上个月,淡丶无欲 让我写一期关于 闭包 的随笔,其实惭愧,我对闭包也是略知一二 ,不能给出一个很好的解释,担心自己讲不出个所以然来. 所以带着学习的目的来写一写,如有错误,忘不吝赐教 . 为什么要有闭 ...

  8. go defer (go延迟函数)

    go defer (go延迟函数) Go语言的defer算是一个语言的新特性,至少对比当今主流编程语言如此.根据GO LANGUAGE SPEC的说法: A "defer" sta ...

  9. Go 延迟函数 defer 详解

    Go 延迟函数 defer 详解 Go 语言中延迟函数 defer 充当着 try...catch 的重任,使用起来也非常简便,然而在实际应用中,很多 gopher 并没有真正搞明白 defer.re ...

随机推荐

  1. ThinkPHP框架学习(一)

    这几天呢,断断续续地在看孙叔华老师的ThinkPHP教程,期间还做了一些其他事情,出去办了点事,总结总结下一学期规划等等,不知不觉间又过去了大半个星期.现在呢,看完了一天的教程,在这里,还是希望稍微总 ...

  2. bzoj千题计划231:bzoj1997: [Hnoi2010]Planar

    http://www.lydsy.com/JudgeOnline/problem.php?id=1997 如果两条边在环内相交,那么一定也在环外相交 所以环内相交的两条边,必须一条在环内,一条在环外 ...

  3. centos安装lrzsz

    yum -y install lrzsz 使用rz打开上传框

  4. 五行代码终极完美解决从IE6到Chrome所有浏览器的position:fixed;以及闪动问题

    这个方法其实已经使用很久了,之前主要在嵌入式WebQQ等产品中用过,现在拿出来分享一下吧,是目前最简洁的方式来实现ie6的position:fixed; 失效bug,以及的其他方法的闪动问题,CSS代 ...

  5. JQuery对CheckBox的一些相关操作

    一.通过选择器选取CheckBox: 1.给CheckBox设置一个id属性,通过id选择器选取: <input type="checkbox" name="myB ...

  6. 【译】使用OpenVAS 9进行漏洞扫描

    本文译自Vulnerability Scanning with OpenVAS 9 part 1: Installation & Setup系列,本文将融合目前已经发表的四个部分. Part ...

  7. JS判断客户浏览器是否是IE8浏览器、jQuery判断浏览器内核

    今天在使用encharts的时候由于要兼容IE8,所以最终决定在非IE8浏览器使用encharts,在IE8使用amcharts.于是需要使用JS判断使用的浏览器版本: function IEVers ...

  8. lucene入门创建索引——(二)

    1.程序宏观结构图

  9. 【源码阅读】Mimikatz相关资料

    Mimikatz GitHub (源码) https://github.com/gentilkiwi/mimikatz Mimikatz GitHub Wiki (包含了一些说明文档) https:/ ...

  10. django-xadmin后台开发

    先通过pip命令行安装django<=1.9版本 示例:pip install django==1.9 从https://github.com/sshwsfc/xadmin下载xadmin源码解 ...