JS 函数的执行时机

98 阅读2分钟

1. 先看一个例子

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

结果打印出6个6

这是什么原因呢?说一说我的理解

  • let声明的变量是块级作用域,作用域在for循环的上一级
  • for循环执行完毕后,才会执行setTimeout中的函数
  • 由于每次for循环都会覆盖上一次的值,所有最后i会在声明的作用域中找到它的值,为6
  • 由于循环了6次,访问到的i都是同一个,所以会打印出6个6

2. 写一个方法打印出不同以上的结果呢

只需要将i声明放在for循环里面

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

结果:0、1、2、3、4、5

发现没有, 仅仅改变 let 的位置, 当 for 和 let 结合,就能有如此奇效, 这就是 JS 给小白的爱。

那还有没有别的办法, 在 ES6 之前,还真有另外的两种方法,一种是利用 setTimeout 的第三个参数,另外一种是利用闭包。

3. 另外两种方法

// 第一种方法
for (var i=0; i<6; ++i) {
    setTimeout(function(i){
        console.log(i)
    }, 0, i)
}

// 第二种方法
for(var i=0; i<6; ++i) {
    !(function(j) {
        setTimeout(function(){
            console.log(j)
        }, 0)
    })(i)
}

任务队列

要实现非阻塞,主要靠异步,怎么实现呢,需要有一个静态的任务队列,存储异步处理完毕后返回的回调函数。

同步任务

在主线程上排队执行的任务,常见的有:

  • 输出,如console.log()
  • 变量声明
  • 同步函数,也就是被调用时不会立即返回,而是等函数内所有任务都做完了再返回的函数。

异步任务

在异步线程上执行的任务,比如setTimeout,常见的还有AJAX等。

工作原理

96afbb2f9642b017d691b575b945dd8.jpg

一开始主线程执行console.log(1),紧接着检测到setTimeout()函数,把它移交给响应的异步线程处理,之后主线程就跳过setTimeout(),继续执行下面的console.log(3);一开始任务队列是空的,主线程中的setTimeout()进入异步线程后,开始执行,经过1秒钟的延迟后,setTimeout()的回调函数进入任务队列,主线程的同步任务执行完毕后,进入了空闲期,于是开始询问任务队列是否有任务需要主线程完成,此时任务队列里存在之前的setTimeout()运行完毕后的回调函数,这个函数被取出到主线程执行,于是回调函数中的console.log(2)内容被执行。(主线程、异步线程和任务队列中其实还有一个中介人叫轮询处理线程Event Loop,为了简化描述这里省略了)。

参考文献

了解JS函数的执行时机