前言
Event Loop 是 JavaScript 异步编程的核心思想,也是前端进阶必须跨越的一关。特别是在 Promise 出现之后,各种各样的面试题层出不穷,花样百出。这篇文章从现实生活中的例子入手,让你彻底理解 Event Loop 的原理和机制
一、为什么 JavaScript 是单线程的?
JavaScript 是一门 单线程 语言,也就是说同一时间只能做一件事。这是因为 JavaScript 生来作为浏览器脚本语言,主要用来处理与用户的交互、网络以及操作 DOM。这就决定了它只能是单线程的,否则会带来很复杂的同步问题。
举个栗子:假设 JavaScript 有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准呢?
既然 Javascript 是单线程的,它就像是只有一个窗口的银行,客户不得不排队一个一个的等待办理。同理 JavaScript 的任务也要一个接一个的执行,如果某个任务(比如加载高清图片)是个耗时任务,那浏览器岂不得一直卡着?为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念。
二、什么是事件循环?
在异步代码完成后仍有可能要在一旁等待,因为此时程序可能在做其他的事情,等到程序空闲下来才有时间去看哪些异步已经完成了。所以 JavaScript 有一套机制去处理同步和异步操作,那就是事件循环 (Event Loop)。
书面来说:
- 所有同步任务都在主线程上执行,形成一个执行栈
- 而异步任务会被放置到 Task Table,也就是上图中的异步处理模块,当异步任务有了运行结果,就将该函数移入任务队列。
- 一旦执行栈中的所有同步任务执行完毕,引擎就会读取任务队列,然后将任务队列中的第一个任务压入执行栈中运行。
总结:主线程不断重复第三步,也就是 只要主线程空了,就会去读取任务队列,该过程不断重复,这就是所谓的 事件循环。
三、宏任务和微任务
任务队列其实不止一种,根据任务种类的不同,可以分为微任务(micro task)队列和宏任务(macro task)队列。宏任务会进入一个队列,而微任务会进入到另一个不同的队列,且微任务要 优于 \color{red}{优于} 优于 宏任务执行。
常见的宏任务和微任务:
- 宏任务:script(整体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)
- 微任务:Promise.then、 MutaionObserver、process.nextTick (Node.js)
四、小试牛刀
代码如下(示例):
console.log(1) setTimeout(function() { console.log(2) }, 0) const p = new Promise((resolve, reject) => { resolve(1000) }) p.then(data => { console.log(data) }) console.log(3)
第一个题介绍简单解析一下:
- nsole.log(1) 是同步任务, 所以立即执行
- setTimeout是宏任务, 所以等微任务做完在执行. (代码 ---> 宏任务队列)
- promise.then是微任务, 所以等主线程执行完毕在执行. (代码 ---> 微任务队列)
- console.log(3) 是同步任务, 所以立即执行. (代码 ---> 执行栈 ---> 输出区域)
- 现在主线程上面的任务全部执行完毕, 然后执行微任务队列的任务, 输出console.log(data)
- 现在主线程上面没有任务, 微任务队列也没有任务, 这才这些宏任务队列, 输出console.log(2)
代码如下(示例):
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');
代码如下(示例):
console.log(1) setTimeout(function() { console.log(2) new Promise(function(resolve) { console.log(3) resolve() }).then(function() { console.log(4) }) }) new Promise(function(resolve) { console.log(5) resolve() }).then(function() { console.log(6) }) setTimeout(function() { console.log(7) new Promise(function(resolve) { console.log(8) resolve() }).then(function() { console.log(9) }) }) console.log(10)
代码如下(示例):
new Promise((resolve, reject) => { resolve(1) new Promise((resolve, reject) => { resolve(2) }).then(data => { console.log(data) }) }).then(data => { console.log(data) }) console.log(3)
console.log(1); async function fnOne() { console.log(2); await fnTwo(); // 右结合先执行右侧的代码, 然后等待 console.log(3); } async function fnTwo() { console.log(4); } fnOne(); setTimeout(() => { console.log(5); }, 2000); let p = new Promise((resolve, reject) => { // new Promise()里的函数体会马上执行所有代码 console.log(6); resolve(); console.log(7); }) setTimeout(() => { console.log(8) }, 0) p.then(() => { console.log(9); }) console.log(10);
总结
提示:这里对文章进行总结:
以上就是今天要讲的内容,本文仅仅简单介绍了事件轮询理解和实践