JavaScript 之 事件循环

79 阅读4分钟

单线程

JavaScript 是一门单线程语言。同一时间只能做一件事,如果出现了某个耗时任务,就可能出现主线程的阻塞,为了防止这种情况,JavaScript就通过同步和异步来处理

调用栈(Call Stack)

调用栈是解释器追踪函数执行流的一种机。

  • 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
  • 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
  • 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。
function greeting() {
  sayHi();
}
function sayHi() {
  return "Hi!";
}

greeting();

image.png

执行流程:

  1. 调用函数greeting()
  2. greeting()添加进调用栈
  3. 执行greeting()函数体内的代码
  4. 调用函数sayHi()
  5. sayHi()添加进调用栈
  6. 执行sayHi()函数体内的代码
  7. 执行greeting()函数体中sayHi()后面的代码
  8. 删除调用栈中的 sayHi() 函数
  9. 执行完greeting()函数体中的代码,返回到greeting()函数调用处,继续执行后面代码
  10. 删除调用栈中的 greeting() 函数

任务队列(Callback Queue)

任务队列是一个事件队列,是一个先进先出的数据结构,排在前面的事件,将先被主线程读取。任务队列中的事件包括用户产生的事件(比如鼠标点击、页面滚动)网络请求、页面渲染等事件。 WebAPIs会将所有事件中已完成事件的回调推到对应的任务队列中

宏任务(Task)

所有在调用栈执行的代码都是宏任务

script, setTimeout, setInterval, setImmediate(node环境下是,浏览器环境不是), requestAnimationFrame(浏览器环境是,node则不是), I/O, UI rendering

微任务(Microtask)

在当前宏任务执行结束后,下个宏任务执行前,执行的就是微任务.

process.nextTick(node环境下是,而浏览器环境下不是), Promise, Object.observe,MutationObserver(在浏览器环境是,而node环境不是)

Event Loop

事件循环流程:

  1. 从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行
  2. 执行完宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止,一次事件循环就结束了
  3. 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止

image.png


function fn(){
  console.log(3);
}

setTimeout(()=>{
  console.log(6);
},1000)

console.log(1);

new Promise(function(resolve, reject) {
  console.log(2);
  resolve()
}).then(function() {
  console.log(5);
})

fn()

console.log(4);
  1. setTimeout()函数添加到调用栈,将回调函数添加到宏任务队列,然后该函数被移除调用栈
  2. console.log(1)添加到调用栈后执行,并移除调用栈
  3. new Promise() 添加到调用栈执行
  4. 执行console.log(2)resolve()
  5. new Promise().then()推入微任务队列
  6. new Promise()移除调用栈
  7. fn()函数添加到调用栈执行
  8. console.log(3)添加到调用栈后执行,并移除调用栈
  9. console.log(4)添加到调用栈后执行,并移除调用栈
  10. 此时调用栈已经清空了,开始执行微任务队列,new Promise().then()添加到调用栈
  11. console.log(5)添加到调用栈后执行,并移除调用栈
  12. new Promise().then()移除调用栈
  13. 此时微任务队列已经清空了,开始执行下一个宏任务,setTimeout callback添加到调用栈
  14. console.log(6)添加到调用栈后执行,并移除调用栈
  15. setTimeout callback移除调用栈

async/await

async function f1(){
  await f2()
  console.log(1);
}

async function f2(){
  console.log(2);
}

f1()

// 2 1

await 前面的代码是同步代码,调用函数时会直接执行。
await f2()就是将f2()包装成Promise,跳出f1()函数执行,等同于:

async function f1(){
  Promise.resolve(f2()).then(()=>{
    console.log(1);
  })
}

练习题

题一

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(function () {
  console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')

// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

题二

async function async1() {
  console.log(1)
  await async2()
  console.log(2)
}
async function async2() {
  console.log(3)
  setTimeout(function () {
    console.log(4)
  })
}
console.log(5)
setTimeout(function () {
  console.log(6)
})
async1()
new Promise(function (resolve, reject) {
  console.log(7)
  setTimeout(function () {
    console.log(8)
  })
  resolve()
}).then(function () {
  console.log(13)
  return new Promise(function (resolve, reject) {
    console.log(9)
    setTimeout(function () {
      console.log(10)
    })
    resolve()
  }).then(() => {
    console.log(11)
    setTimeout(function () {
      console.log(12)
    })
  })
})
console.log(14)


// 5 1 3 7 14 2 13 9 11 6 4 8 10 12