聊聊事件循环机制

551 阅读4分钟

同步和异步

我们都知道代码是一行一行执行的,但是有时候如果前面的代码执需要花更长的时间,所以,我们可以让后面的代码先执行,而不会让页面加载的很慢,然后JavaScript是单线程语言,但是他有同步和异步概念,简要了解同步和异步的概念

  • 同步:如果在一个函数返回的时候,调用者就能够得到预期结果,那么这个函数就是同步的;
  • 异步:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。 为什么JavaScript是单线程的了,我们就需要来聊聊单线程的好处了

js引擎在JS运行时会阻塞UI的渲染(渲染引擎的工作)==> JS引擎线程和渲染页面的线程是互斥的 因为JS可以修改dom结构,如果JS执行的时候UI线程还在工作,就可能导致不安全的渲染ui,得益于JS就是单线程运行的,可以达到节省运行内存,节约上下文切换的时间

事件循环

  • 同步任务和异步任务 所有的同步任务都会在执行栈中,JavaScript在按顺序执行执行栈中的方法时,每次执行一个方法,都会为它生成独有的执行环境(上下文),当这个方法执行完成后,就会销毁当前的执行环境,并从栈中弹出此方法,然后继续执行下一个方法。 所有的异步任务都会放到任务队列中,然后一个一个执行 所以他的执行顺序就是先执行同步任务,等到同步任务执行完后,然后再到任务队列中去执行异步任务

看个简单的例子

console.log('代码开始执行');
setTimeout(function(){
    console.log('定时器开始执行')
});
console.log('代码执行结束');

结果是: 代码开始执行->代码执行结束->定时器开始啦,因为setTimeout是异步任务,所以就会后后执行,

在看一个例子

setTimeout(function(){
    console.log('2')
});
for(let i =0;i<3000;i++){
    console.log(1);
}
console.log(3);

它会输出3000个1之后,在输出3,然后才输出2,这也就说明先执行同步任务,等到同步任务执行完后,然后再到任务队列中去执行异步任务

宏任务与微任务

任务队列还可以分成宏任务月微任务,微任务就是一个跟屁虫,一直跟在当前宏任务后面,代码执行到一个微任务,一个接着一个。

我们常见的宏任务有:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境),ajax,读取文件

微任务有:Promise.then、MutaionObserver、process.nextTick(Node.js 环境); 优先级 process.nextTick > promise.then > setTimeout > setImmediate

我们需要记住这些常见的宏任务与微任务

  • process.nextTick

console.log('代码开始执行');
setTimeout(() => {
    console.log('timeout');
}, 0);

Promise.resolve().then(() => {
    console.error('promise')
})

process.nextTick(() => {
    console.error('nextTick')
})
console.log('代码执行结束');

执行结果 代码开始执行 -> 代码执行结束 -> nextTick -> promise -> timeout

上面提到了process.nextTick(),它是node中新引入的一个任务队列,它会在上述同步阶段结束时,在进入下一个阶段之前立即执行。

  • await

关于async await async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。 当await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。它是执行完await之后,直接跳出async函数,执行其他代码。其他代码执行完毕后,再回到async函数去执行剩下的代码,

经典题

先来看看下面这到经典例子,看看你是否能做出来

console.log('script start')

async function async1() {
console.log('async1'); //同步
await async2()
console.log('async1 end') //异步
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
process.nextTick(() => {
    console.error('nextTick')
})
new Promise(resolve => {
console.log('Promise') //同步
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')

新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,答案是 script start -> async1 -> async2 end -> Promise -> script end -> nextTick -> async1 end -> promise1 -> promise2 -> setTimeout 为什么会是这样的结果了,我们慢慢来分析一下

  • 首先执行同步代码,输出script start,
  • 调用async1 输出async1 ,
  • 调用async2 输出async2 end `,
  • 遇到setTimeout,产生一个宏任务
  • 执行Promise,输出Promise 接着输出script end 同步任务都执行完了,然后去到异步任务队列
  • 执行process.nextTick,生第一个微任务
  • 执行await后面产生的微任务 async1 end
  • 遇到then,产生新的微任务
  • 开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务
  • 执行产生的微任务,输出promise2,当前微任务队列执行完毕
  • 最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

总结:

微任务就是一个跟屁虫,一直跟在当前宏任务后面 ,代码执行到一个微任务,一个接着一个

任务的优先级 process.nextTick > promise.then > setTimeout > setImmediate

执行顺序:

- 首先执行同步代码,
- 当执行完所有的同步代码后,执行栈为空,去查询是否有异步代码需要执行
- 执行所有的微任务
- 当执行完所有的微任务后
- 开始下一轮Event-loop,执行宏任务中的异步代码
同步--> 异步--> 微任务--> 宏任务