根据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. python XML梳理

    导入ElementTree模块 import xml.etree.ElementTree as ET 为了创建一个element实例,使用Element 构造函数或者SubElement()工厂函数. ...

  2. Easy2Boot-小清新教程

    Author:KillerLegend Date:2014.8.14 From:http://www.cnblogs.com/killerlegend/p/3913614.html 之所以说是小清新, ...

  3. 高斯—若尔当(约当)消元法解异或方程组+bitset优化模板

    高斯消元法是将矩阵化为上三角矩阵 高斯—若尔当消元法是 选定主元后,将主元化为1,枚举除主元之外的所有行进行消元 即将矩阵化为对角矩阵,这样不用回代 bitset<N>a[N]; int ...

  4. c# yield关键字原理详解

    c# yield关键字的用法 1.yield实现的功能 yield return: 先看下面的代码,通过yield return实现了类似用foreach遍历数组的功能,说明yield return也 ...

  5. BFS简单题套路_Codevs 1215 迷宫

    BFS 简单题套路 1. 遇到迷宫之类的简单题,有什么行走方向的,先写下面的 声明 ; struct Status { int r, c; Status(, ) : r(r), c(c) {} // ...

  6. 【整理】HTML5游戏开发学习笔记(3)- 抛物线运动

    1.预备知识(1)Canvas旋转的实现过程 window.onload = function(){ var ctx = document.getElementById('canvas1').getC ...

  7. Hive笔记之Fetch Task

    在使用Hive的时候,有时候只是想取表中某个分区的前几条的记录看下数据格式,比如一个很常用的查询: select * from foo where partition_column=bar limit ...

  8. vue中,写在methods里的B方法去调A方法的数据,访问不到?

    今天在写项目的时候,发现了一个京城性忽略的问题,在vue的methods的方法里面定义了两个方法,如下: getTaskList() { api.growthDetails.taskList({ ap ...

  9. Unity3d 常用代码

    //创建一个名为"Player"的游戏物体 //并给他添加刚体和立方体碰撞器. player=new GameObject("Player"); player. ...

  10. elasticsearch分别在windows和linux系统安装

    WINDOWS系统安装1.安装JDKElastic Search要求使用较高版本JDK,本文使用D:\DevTools\jdk1.8.0_131,并配置环境变量 2.安装Elastic Search官 ...