JS函数的执行时机

思考

代码为什么会打印 6 个 6

1
2
3
4
5
6
let i = 0;
for(i = 0; i < 6; i++) {
setTimeout( () => {
console.log(i)
}, 0);
};

分析

在上面代码中,for 循环是同步代码,setTimeout 是异步代码,JS 按照从上到下的执行顺序执行同步代码,而异步代码被插入到任务队列中。

当执行完同步代码(for 循环),JS 会去执行异步代码(setTimeout)。

在每次 for 循环中,都将异步代码(setTimeout)放入任务队列中,所以任务队列中有 6 个 setTimeout 即有 6 个 console.log(i)

在每次 for 循环中将 setTimeout 里的代码 console.log(i) 放入任务队列时,i 的值是不一样的,当 JS 引擎开始执行任务队列中代码时,会在当前作用域中找变量 i ,但当前 for 循环的作用域中没有对变量 i 的进行定义,这个时候会在创造该函数的作用域中寻找 i,找到的是 let i,这时的 i 时全局变量,并且值已经确定为 6。所以打印出 6 个 6。

执行流程:

for(i=0) ==> for(i=1) ==> for(i=2) ==> for(i=3) ==> for(i=4) ==> for(i=5) ==> for(i=6) ==> console.log(6)x6

解决方法一 let

1
2
3
4
5
for( let i = 0; i < 6; i++ ){
setTimeout( ()=> {
console.log(i);
}, 0);
}

let 的作用域是块作用域,能作用到 for 循环的子块中。

let 的作用于是块作用域,所以 setTimeout 被放到 任务队列的同时,let 定义的 i 值 也会跟随 setTimeout 进入队列。所以每次循环后队列里的 setTimeout 里的 i 值是不一样的。而 var 定义的 i 是无法进入的。(浅显易懂)

for 循环头部的 let 不仅将 i 绑定到 for 循环中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值被重新赋值。setTimeout 里面的函数属于一个新的域,通过 var 定义的变量或全局变量是无法传入到这个函数执行,通过使用 let 来声明块变量能作用于这个块,所以箭头函数就能使用 i 这个变量,所以每次的 i 值不一样。

解决方法二 使用立即执行函数,即闭包

1
2
3
4
5
6
7
8
let i = 0;
for( i = 0; i < 6; i++){
(function (j){
setTimeout( () => {
console.log(j);
}, 0);
})(i);
}

因为 setTimeout 是异步执行,所以让它立即执行就可以了。

通过闭包,将 i 的变量驻留在内存中,当输出 j 时,引用的是外部函数的变量值 i,i 的值是根据循环来的,执行 setTimeout 时已经确定了里面的的输出了。

解决方法三 setTimeout 第三个参数

1
2
3
4
5
6
7
let i = 0;
for (i = 0; i < 6; i++){
setTimeout( (i) => {
console.log(i);
}, 0, i);
}
// 将每次的 i 值传入作用域。

解决方法四 try catch

1
2
3
4
5
6
7
8
9
10
11
let i = 0;
for(i = 0; i < 6; i++){
try{
throw i
}catch(i){
setTimeout( () => {
console.log(i)
}, 0);
}
}
// 将 i 作为异常抛出,传递给 setTimeout

JS执行机制

首先,JS是单线程环境,代码从上到下依次执行。这种执行方这也被称作是“同步执行”。(同一时间 JS 只能执行一段代码,如果这段代码要执行很长时间,那么之后的代码只能尽情地等待它执行完才能执行)。

但 JS 中引进了异步机制。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有主线程上的任务执行完了,才通知”任务队列”,任务队列中的任务才会进入主线程执行。

执行栈

当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入任务队列中,等待主线程读取,遵循先进先出原则。

执行任务队列中的某个任务,这个被执行的任务就称为执行栈。

主线程

要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。

主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。

当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。

当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。

JS 异步执行的运行机制

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列。那些对应的异步任务,进入执行栈开始执行。
  4. 主线程不断重复上面的第三步。