# 事件循环

# 事件循环的定义

​ JavaScript的代码执行模式是单线程,它是通过调用栈来执行函数的,但除此之外,还要依靠任务队列来完成其他代码的执行。整个执行的过程,我们称之为事件循环。任务队列是分有两类的,一类为宏任务队列,另一类为为任务队列。在最新的标准中,他们又称为task与jobs。

# 浏览器任务的分类

# 宏任务有以下几种

  1. setTimeout
  2. setInternal
  3. setImmediate
  4. I/O
  5. UI render
  6. script(整体代码)

# 微任务有以下几种

  1. Promise
  2. async/await
  3. process.nextTick
  4. MutationObserver(html5新特性)

# 浏览器事件循环执行机制

任务队列中,宏任务与微任务之间的执行顺序如下:

  1. 执行宏任务(整体script段),过程中遇到其他宏任务,放入宏任务队列。
  2. 执行微任务,若执行过程中产生了新的微任务,则放入微任务队列。
  3. 执行之前产生的新的微任务。
  4. 执行宏任务队列。
  5. 两个队列都为空,执行完毕

总体流程就是,执行宏1(产生宏1.1,push macro queue),执行微1(产生微1.1,push micro queue)。执行微2结束(micro queue为空),执行宏2(macro queue为空)。执行完毕。

`宏任务(整块代码)->微任务->子微任务->子宏任务->执行完毕

# 例题

# 题1

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')
}, 0)

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')

执行顺序:

  1. 宏1(script 片段)
  2. console语句,输出 script start
  3. 微1(async2),此时执行环境是async1,输出 async2 end,保留async1上下文。
  4. 宏1.1(setTimeout) 推入宏队列
  5. 微2(Promises),输出 Promise,产生微2.1(promise1)并 推入微队列
  6. console语句,输出 script end
  7. 微任务队列不为空,执行微队列,微2.1(promise1),输出 promise1. 产生微2.1.1(promise2)并推入微队列
  8. 微2.1.1(promise2),输出 promise2
  9. 微队列完,执行权回到async1上下文,输出 async end
  10. 执行宏队列,宏1.1(setTimout),输出 setTimeout
理论与实践不一致
----理论上----
//script start
//async2 end
//Promise
//script end
//promise1
//promise2
async end 原位置
//setTimeout

---实际上---
//script start
//async2 end
//Promise
//script end
async end  这一步发生了改变
//promise1
//promise2
//setTimeout

TIP

可以看到 async end的打印,理论和实践上有偏差。但是在题2中,理论又和实践一致。为什么会这样,将在问题萌生与解决中说明



# 题2

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
        console.log('async2 end1')
    })
}
async1()

setTimeout(function() {
    console.log('setTimeout')
}, 0)

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function() {
    console.log('promise1')
})
.then(function() {
    console.log('promise2')
})

console.log('script end')

执行顺序如下:

  1. 输出script start

  2. 执行async1中的async2。输出async2 end, 将 返回的promise推入微队列

  3. setTimeout推入宏队列

  4. 输出Promise, promise1推入微队列

  5. 输出script end

  6. 输出 async2 end1

  7. 输出promise1,将promise2推入微队列

  8. 输出promise2

  9. 执行权返回至async1,输出async1 end

  10. 微队列为空,执行宏队列,输出setTimeout

    理论与实践一致,浏览器、node运行也是如此
    // script start
    // async2 end
    // promise
    // script end
    // async2 end1
    // promise1
    // promise2
    // async1 end
    // setTimeout
    

# 问题的萌生与解决

上面两道题中,执行的逻辑判断都逐一说明,但题1的返回结果与理论中不同。于是我比对了两题的不同:题2与题1的差别,其实就在于题2中的async2函数多了个异步返回值。关键点就在于这个异步返回值,我猜测将会影响async中await后续代码的执行顺序。我在上述两题的基础上,新增两个例子,并验证我的猜测。

# 示例1,所有async不带有异步返回值。

console.log("script start");

async function async1() {
  await async2();
  console.log("async1 end");
}
async function async2() {
  await async3();
  console.log("async2 end");
}
async function async3() {
  console.log("async3 end");
}
async1();

setTimeout(function () {
  console.log("setTimeout");
}, 0);

new Promise((resolve) => {
  console.log("Promise");
  resolve();
})
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");

//script start
//async3 end
//Promise
//script end
async2 end  --可以看到在第一轮从上到下执行完毕时,紧跟的是async2,async2后跟着的时promise,可以说明async2在第一轮执行判断时,被注册到微队列中。 
promise1 -- 打印出promise1的这一轮循环,async1函数内的 console.log("async1 end")同样被注册到微队列中。
async1 end -- 执行上一轮给到的微任务。
//promise2
//setTimeout

可以注意到,async在没有返回值时,await后面的命令被视为微任务注册到队列中。并且,他们的顺序是按照 await async3()之后,再到await async2()之后,每一轮呈逆序注册。


# 示例2,所有async带有异步返回值。

console.log("script start");

async function async1() {
  await async2();
  console.log("async1 end");
}
async function async2() {
  await async3();
  console.log("async2 end");
}
async function async3() {
  console.log("async3 end");
  return Promise.resolve().then(() => {
    console.log("async3 end1");
  });
}
async1();

setTimeout(function () {
  console.log("setTimeout");
}, 0);

new Promise((resolve) => {
  console.log("Promise");
  resolve();
})
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");

//script start
//async3 end
//Promise
//script end
async3 end1 异步返回值被注册为微任务
//promise1
//promise2
async2 end  带有异步返回值后,await后的命令将被视作同步语句,且也是逆序执行。
async1 end  带有异步返回值后,await后的命令将被视作同步语句,且也是逆序执行。
//setTimeout

在带有异步返回值后,await后的语句不再被视为微任务,于是排到了promise2(最后一个微任务)之后。

# Node中的事件循环

浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。

# node任务的分类

宏任务有下列:

  1. setTimeout
  2. setInterval
  3. setImmediate
  4. script(整体代码)
  5. I/O操作等。

微任务有下列:

  1. process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
  2. new Promise().then(callback)等。

# Process.nextTick

process.nextTick 是一个独立于 eventLoop 的任务队列。

在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。

# node事件循环执行机制

node中的事件循环执行机制与其版本有关。node v11之前的版本与浏览器无异。但在v11之后(包括v11)的版本,如果宏任务(setTimeout、setInternal、setImmediate等)中有微任务,将执行里面的微任务队列(但如果有process.nextTick,则先于微任务执行)。之后再往下执行。

# node例1

setImmediate(() => {
    console.log('timeout1')
    Promise.resolve().then(() => console.log('promise resolve'))
    process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
node v11之前
//timeout1 
//timeout2
//timeout3
//timeout4
//next tick1
//next tick2
//promise resolve

node v11之后
//timeout1 
//next tick1
//promise resolve
//timeout2
//next tick2
//timeout3
//timeout4

# node例2

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

node v11之前
// timer1
// timer2
// promise1
// promise2

node v11之后 但要看第一个定时器执行完后,第二个定时器是否在执行队列中
第二个定时器还没执行完,则实际上只有第一个定时器进行事件循环。
// timer1
// promise1
// timer2
// promise2

第二个定时器执行完,则按照v11之后的执行机制来,与浏览器执行机制一致。
// timer1
// timer2
// promise1
// promise2

# node和浏览器eventLoop的主要区别

两者最大的区别在于node的微任务执行顺序取决于node的版本,而浏览器则保持着恒定的执行顺序。

# 总结

  1. 当存在async await微任务时,注意async的返回值是否为异步,若为异步,则执行结果将有相应的变化。
  2. node的微任务执行顺序取决于node的版本在11之前,还是执行之后。
  3. 事件循环的执行机制大体为:宏-微-宏,微任务有产生子微任务,则宏-微-微-宏,以此类推。

# 参考文章

  1. js中的eventLoop (opens new window)