简单理解Event Loop

594 阅读3分钟

简单理解Event Loop

由于JavaScript是单线程,所以在同一时间只能完成一件事情。打个比方,程序在进行大量计算或进行IO操作,是无法进行DOM的UI渲染的。

我们可以看看以下的伪代码

const val1 = $.get('/test1')
const val2 = $.get('/test2')
const val3 = $.get('/test3')

console.log(val1) // 1
console.log(val2) // 2
console.log(val3) // 3

我们假设JavaScript只支持同步顺序执行(网络延迟2s),那么上述代码就会是这样。 程序用6s进行网络请求操作(期间页面卡死,无法进行任何操作,UI无法渲染),然后在控制台中输出1 2 3。如果途中有一个请求一直没有回包,那么这个程序就一直卡死了。

真实中我们看到浏览器页面没有因为网络IO导致页面卡死,这是因为JavaScript通过组合使用执行栈任务队列(task queue)解决异步问题。

顺序执行栈

首先我们先一起看一下栈调用。

function func1(n) {
  console.log(n + 1);
}

function func2(m, callback) {
  console.log(m + 1);
  callback(m + 1);
}

function func3(m, n, callback) {
  console.log(m + n);
  callback(m + n);
}

func3(1, 2, (res) => {
  func2(res, res2 => {
    func1(res2);
  });
});

程序很简单,输出

3
4
5

我们一起看看程序在栈中表现。

任务队列

接下来我们一起看看任务队列的概念,很简单的,哈哈。

function main() {
  console.log(1);
  setTimeout(function cb() {
    console.log(3);
  }, 1000);
  console.log(2);
}

main();

结果也是显而易见的,在输出1和2,等待约1秒输出3

1
2
3

main进栈

console.log(1)进栈,控制台打印1后,console.log(1)出栈。

setTimeout(cb, 1000)进栈,发现setTimeout是浏览器api,因此把这段代码交由浏览器执行,1000ms后cb函数进入任务队列。

setTimeout交由浏览器执行的时候,console.log(2)进栈,控制台打印2后,console.log(2)出栈,然后main函数执行完毕,main函数出栈。

此时执行栈为空,任务队列存在cb()函数。cb()函数被压入栈中执行,输出3后,console.log(3)cb()依次弹出栈,程序执行完毕。

好,看到这里,把上面的代码的1000延时换成0,看看结果是什么。

function main() {
  console.log(1);
  setTimeout(() => {
    console.log(3);
  }, 0);
  console.log(2);
}

main();

套用上面的分析我们可以很快说出答案

1
2
3

macro task 与 micro task

任务队列(task queue)可以分为两种

  • 宏任务队列 macro task queue
    • setTimeout
    • setInterval
    • requestAnimationFrame
    • I/O
    • UI rendering
  • 微任务队列 micro task queue
    • Promise
    • Object.observe
    • MutationObserver

浏览器中,代码执行的顺序如下:

  1. 优先执行stack中的代码,遇到上述特殊的api,则放置到任务队列中。例如,promise则放置到micro task,setTimeout则放到macro task。
  2. 执行栈为空时,先查看micro task队列,若队列中存在任务,则从队列首部获取一个任务放入Stack中执行,队列长度减1。若此时又产生一个微任务,则继续放到队尾。
  3. 重复2的操作,直到micro task队列为空。
  4. 查看macro task,把队首任务放入Stack中执行。
  5. 重复4,直到Stack为空。
function func1() {
  return new Promise((resolve, reject) => {
    console.log(8);
    resolve(9);
  });
}

async function func2() {
  const num = await func1();
  console.log(num);
}

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
});

func2();

new Promise((resolve, reject) => {
  console.log(4);
  resolve(5);
}).then((data) => {
  console.log(data);
});

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

console.log(7);

套用上面的规则,测试一下能否得到正常的结果(注意,必须要在浏览器环境测试,node的结果会不一样的,因为node的event loop和浏览器的不太一样)

1
8
4
7
9
5
2
3
6

参考资料