闭包应用之延迟函数setTimeout

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了闭包应用之延迟函数setTimeout相关的知识,希望对你有一定的参考价值。

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

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

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

一、用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,这种理解是不对的。

因此,即使我们将延迟时间设置为0,它定义的操作仍然需要等待所有代码执行完毕后才开始执行。这里的延迟时间,并非相对于setTimeout执行这一刻,而是相对于其他代码执行完毕这一刻。

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

代码执行时,立即输出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的主要内容,如果未能解决你的问题,请参考以下文章

Javascript setTimeout,闭包

关于setTimeout的妙用

setTimeout中所执行函数中的this,永远指向window

setTimeout 函数执行代码没有任何延迟。 Javascript [重复]

JavaScript 之 定时器 延迟器

for循环,定时器,闭包。