终于搞懂了javaScript的事件循环机制

160 阅读6分钟

事件循环机制(Event Loop)

一、介绍

JavaScript是一门单线程语言,指主线程只有一个。

为什么Javascript号称是单线程,但是又能支持异步非阻塞执行呢?

在整个JavaScript执行线程中只有一个调用栈,也就是只有一个时间线索,即每调用一个函数就只能往这个栈进行压栈,最后调用的函数在栈顶。执行完成后从栈顶逐个返回。

JavaScript没有提供创建新线程的方法,让我们可以像上面多线程一样创建多一个线程出来,让线程A执行函数FA,线程B执行函数FB。

JavaScript执行线程本身是单线程,但是整个浏览器不是单线程的。V8引擎在检测到异步调用,如setTimeout等WebAPIs调用后,会将其交给浏览器的其他线程进行执行处理,完了后再通过事件循环机制返回执行线程执行回调。

浏览器是多进程的,浏览器每一个打开一个Tab页面(网页)都代表着创建一个独立的进程(至少需要四个,若页面有插件运行,则五个)。渲染进程(浏览器内核)是多线程的,也是浏览器的重点,因为页面的渲染,JS执行等都在这个进程内进行。

浏览器的进程主要包括:

  • GUI渲染线程
  • JS引擎线程
  • 定时触发器线程
  • 事件触发线程
  • 异步http请求线程

浏览器的事件循环分为同步任务和异步任务:所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微任务(micro-task)。下面的整个执行过程就是事件循环

宏任务大概包括::script(整块代码)、setTimeoutsetIntervalI/OUI交互事件setImmediate(node环境)

微任务大概包括::new promise().then(回调)、MutationObserver(html5新特新)、Object.observe(已废弃)、process.nextTick(node环境)

ps:若同时存在promise和nextTick,则先执行nextTick

二、执行过程

1.先从script(整块代码,即一次宏任务)开始第一次循环执行;

2.接着对同步任务进行执行,直到调用栈被清空;

3.一次宏任务(script)执行结束后,需要去执行该次宏任务中生成的所有的微任务,直到所有微任务执行完毕,微任务队列清空;

4.此时微任务队列已经清空,再次从宏任务队列按照先入先出的规则取出一个宏任务任务执行;

5.每个宏任务完成后需要执行该次宏任务中生成的微任务队列

6.如果在执行微队列任务的过程中,又产生了微任务,那么会加入微任务队列的队尾,也会在当前的周期中执行,直到两个任务队列全部执行完毕,代码结束。

以上过程相当于任务在不断的宏任务和微任务之间循环,所以称为事件循环。

三、async/await

async/await也经常会用于处理异步。

async没什么好说的,一个修饰符,可以单独出现,使函数的返回值变成一个Promise对象。

await修饰符只能放在async函数内部,await关键字的作用就是获取Promise中返回的内容,获取的是Promise函数中resolve或者reject的值,如果await后面是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果await 后面并不是一个Promise的返回值,则会按照同步程序返回值处理。

1.后面跟Promise对象

function sleep(second) {
    return new Promise((resolve, reject) => {
        console.log('test')
        setTimeout(() => {
            resolve(' enough sleep~');
        }, second);
    })
}
async function awaitDemo() {
    let result = await sleep(2000);
    console.log("123")
    console.log(result);
}
awaitDemo();
console.log('321');// 立刻打印test和321,两秒之后会被打印出来 '123'和' enough sleep~'

2.后面跟正常的表达式

function sleep(second) {
    setTimeout(() => {
        console.log(' enough sleep~');
    }, second);
    }
async function awaitDemo() {
    let result = await sleep(2000);
    console.log("123");
}
awaitDemo();
console.log('321');//立即按顺序输出321和123,两秒后输出' enough sleep~'

另外,async修饰的函数会返回一个Promise对象,当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

以下的例子中1比2先打印就用到了这个知识点,有兴趣可以研究以便于加深理解

async function test1() {
  let a = await 1
  console.log('2');
  let c = await new Promise(resolve => {
    setTimeout(() => {
      resolve('setTimeout')
      console.log('test2')
    }, 3000);
    resolve('3')
  })
  console.log('c:', c);
}
test1()
console.log('1');
setTimeout(() => {
  console.log('test1');
})
// 1、2、c:3、test1、(三秒后)test2

四、参考

已经了解了上述的基础知识,就可以尝试做以下例题了。

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('asnyc1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(() => {
    console.log('setTimeOut');
}, 0);
async1();
new Promise(function (reslove) {
    console.log('promise1');
    reslove();
}).then(function () {
    console.log('promise2');
})
console.log('script end');

答案:script start => async1 => start => async2 => promise1 => script end => asnyc1 end => promise2 => setTimeOut

解析:

  1. 整个代码片段(script)作为一个宏任务执行,其内部按照事件循环机制的顺序执行代码。

  2. 同步代码script start最先被打印

  3. setTimeout作为异步中的宏任务被加入宏任务队列进行等待

  4. 执行async1(),根据async/await的用法,如果await后面是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果await后面是正常的表达式则立即执行,await后面跟promise对象的这种情况可以将该函数剩余的代码看作是Promise.resolve().then()的内容加入到微任务队列中。

    等到整个代码片段(script)完成再接着执行函数后面的内容,所以先打印async1 start,再执行async2(),打印async2,此时函数返回,后面的内容阻塞。

  5. 执行 new Promise,(已知promise本身是同步,但是promise的回调then和catch是异步的),先输出promise1,然后将resolve()放入微任务异步队列

  6. 执行console.log('script end'),输出script end

  7. 此时整个代码片段(script),即一次宏任务已经结束,当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件,所以将之前微任务队列中的队列按照先入先出的规则运行,先打印asnyc1 end,再打印promise2

  8. 最后执行setTimeout,输出了settimeout