JS函数的异步

41 阅读3分钟

setTimeout()基础

setTimeout 函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

var timerId = setTimeout(func|code, delay)

上面代码中,setTimeout 函数接受两个参数,第一个参数 func|code 是将要推迟执行的函数名或者一段代码,第二个参数 delay 是推迟执行的毫秒数。

需要注意的是,推迟执行的代码必须以字符串的形式,放入 setTimeout,因为引擎内部使用 eval 函数,将字符串转为代码。如果推迟执行的是函数,则可以直接将函数名,放入 setTimeout。一方面 eval 函数有安全顾虑,另一方面为了便于 JavaScript 引擎优化代码,setTimeout 方法一般总是采用函数名的形式,就像下面这样。

function func(){
  console.log(2);
}
setTimeout(func,1000);
// 或者
setTimeout(function (){console.log(2)},1000);

分析 for 循环中的 setTimeout 函数

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },3000)
}

说起事件循环,不得不提起任务队列。事件循环只有一个,但任务队列可能有多个,任务队列可分为宏任务(macro-task)和微任务(micro-task)。注意进入到任务队列的是具体的执行任务的函数。比如上述例子 setTimeout()中的 console.log 函数。

XHR 回调、事件回调(鼠标键盘事件)、setImmediate、setTimeout、setInterval、indexedDB 数据库操作等 I/O 以及 UI rendering 都属于宏任务(也有文章说 UI render 不属于宏任务,目前还没有定论),process.nextTick、Promise.then、Object.observer(已经被废弃)、MutationObserver(html5 新特性)属于微任务。

另外不同类型的任务会分别进入到他们所属类型的任务队列,比如所有 setTimeout()的回调都会进入到 setTimeout 任务队列,所有 then()回调都会进入到 then 队列。当前的整体代码我们可以认为是宏任务。事件循环从当前整体代码开始第一次事件循环,然后再执行队列中所有的微任务,当微任务执行完毕之后,事件循环再找到其中一个宏任务队列并执行其中的所有任务,然后再找到一个微任务队列并执行里面的所有任务,就这样一直循环下去。

再回过头来看上面那个问题,理解了事件循环的机制,问题就很简单了。for 循环时 setTimeout()不是立即执行的,它们的回调被 push 到了宏任务队列当中,而在执行任务队列里的回调函数时,变量 i 早已变成了 6。

让变量 i 执行看起来符合人们的逻辑

  1. 利用 ES 6 引入的 let 关键字

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

    for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

  2. 用闭包

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

    利用函数创建一个局部变量,保留了调用时的局部变量 i。

  1. 利用 bind 函数
     let i = 0;
     for (i=0; i<6; i++) {
         setTimeout( function timer(i) {
             console.log(i);
         }.bind(null,i), i*1000 );
     }
    
    如果 bind 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为 window)。bind 还可以接受更多的参数,将这些参数绑定原函数的参数。
  1. 利用 setTimeout 第三个参数

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

setTimeout 函数第三个参数及以后的参数都可以作为 timer 函数的参数。