为什么出现事件循环
在了解浏览事件循环之前,我们首先要弄明白为什么会出现事件循环?为什么会产生消息队列?
第一版线程模型
- 任务 1:1+2
- 任务 2:20/5
- 任务 3:7*8
- 任务 4:打印出任务 1、任务 2、任务 3 的运算结果
void MainThread(){ int num1 = 1+2; // 任务 1 int num2 = 20/5; // 任务 2 int num3 = 7*8; // 任务 3 print(" 最终计算的值为:%d,%d,%d",num,num2,num3); // 任务 4 }
当我们需要处理以上四个任务的时候,我们会把以上任务按照顺序写进主线程里,等线程执行时,这些任务会按照顺序在线程中依次被执行;等所有任务执行完成之后,线程会自动退出。
第二版线程模型(引入事件循环)
在任务处理过程中,不是所有任务都是提前准备好的。我们可能在任务执行的过程中,接受到一个新的任务,这时候我们就要引入事件循环,来监听是否有新的任务。
现在的线程模型是这样
这时候,我们就发现了一个问题。渲染进程会频繁的接受到IO线程的消息,就会造成页面渲染的卡顿。那么我们如何改进呢?
第三版线程模型(引入消息队列)
经过改造之后,任务执行可分为三个步骤
- 添加一个消息队列
- IO线程会产生任务放进队尾
- 渲染主进程会循环轮询消息队列
区分宏任务和微任务的必要性
了解了什么是时间循环之后,我们来谈谈为什么要有宏任务和微任务?
处理事件的优先级
比如当我们想要监听DOM元素的删除和修改等,当DOM元素发生变化的时候,我们应该是想立刻看到页面的变化。如果我们把DOM元素的优先级设置成最高,直接放到消息队列的队首,当操作DOM的时间很长时,其他任务就会在消息队列中长时间的等待,造成效率的降低。如果我们放到队尾,又会影响实时性,因为消息队列中可能有很多任务在等待中。
针对这种情况,宏任务和微任务就应运而生。我们将消息队列中的任务称为宏任务,每个宏任务在执行的过程中会产生相应的微任务队列。宏任务执行完成之后,渲染引擎并不会着急执行下一个宏任务,而是执行当前宏任务产生的微任务队列。
事件循环的执行过程
当我们大致了解了事件循环和宏任务,微任务之后,我们来了解一下事件循环具体的执行过程。
宏任务
(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
微任务
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
事件循环执行过程
代码输出题
单独来讲的话太抽象了,我们直接来看几个代码输出题吧
题一
console.log('script start') let promise1 = new Promise(function (resolve) { console.log('promise1') resolve() console.log('promise1 end') }).then(function () { console.log('promise2') }) setTimeout(function(){ console.log('settimeout') }) console.log('script end')
首先,script(整体代码)是一个宏任务,先执行的是整体代码,先输出 'script start' ,然后再执行new Promise里面executor函数, 输出 'promise1' ,再输出 'promise1 end' 然后执行resolve()。 然后将then()里面的函数添加到微任务队列中,再执行setTimeout,并将添加到下一个宏任务队列里面。 此时, script整体代码(宏任务)执行完毕,然后再执行微任务队列,输出 'promise2' 。此时微任务执行完毕, 执行下一个宏任务队列, 输出 'settimeout'。
题二
在看题二之前,我们再复习一下Promise
resolve(参数)有以下几种情况
- 1.普通的值或者对象
- 2.传入一个promise,那么当前Promise的状态由传入的Promise决定,相当于状态进行了移交
- 3.传入一个对象,并且这个对象中有then方法(thenable),那么也会执行该then方法,并且由该then方法决定后续状态
第二种情况,传递一个promise,promise的状态由传入的newPromise的状态决定
const newPromise = new Promise((resolve,reject) => { // resolve('aaa'); // reject('err') }) new Promise((resolve, reject) => { resolve(newPromise) }).then(res => { console.log(res) },err => { console.log(err) })
第三种情况,传入对象中有then方法
new Promise((resolve,reject) => { const obj = { then:function (resolve,reject) { reject('err') } } resolve(obj) }).then((res)=> { console.log('res',res) },err => { console.log('err',err) }) // 输出 err err
好了,我们可以正式来看第二题了
new Promise(resolve => { resolve(1); Promise.resolve().then(() => console.log(2)); console.log(4) }).then(t => console.log(t)); console.log(3);
都看到这里了,相信大家肯定是知道先输出4 3 ,那么2 1是谁先输出呢?
在阮一峰老师的Es6中,Promise.resolve()
方法允许调用时不带参数,直接返回一个resolved
状态的Promise 对象。
需要注意的是,立即resolve()
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。es6.ruanyifeng.com/#docs/promi…
这段代码的流程大致如下:
- script 任务先运行。首先遇到
Promise
实例,构造函数首先执行,所以首先输出了 4。此时 microtask 的任务有t2
和t1
- script 任务继续运行,输出 3。至此,第一个宏任务执行完成。
- 执行所有的微任务,先后取出
t2
和t1
,分别输出 2 和 1 - 代码执行完毕
综上,上述代码的输出是:4321
为什么 t2
会先执行呢?理由如下:
- 根据 Promises/A+规范:
实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在
then
方法被调用的那一轮事件循环之后的新执行栈中执行
所以,t2
比 t1
会先进入 microtask 的 Promise
队列。
自己思考一下撒
setTimeout(()=>{ console.log('setTimeout') },0) Promise.resolve().then(()=>{ console.log('promise1') Promise.resolve().then(() => { console.log('promise2') }) }) console.log('main')
let thenable = { then: function(resolve, reject) { console.log(0) resolve(42); } }; new Promise(resolve => { resolve(1); Promise.resolve(thenable).then((t) => { console.log(t) }); console.log(4) }).then(t => { console.log(t) }); console.log(3);
自己看了些资料然后总结了一下事件循环,如果有不对的地方欢迎大家一起交流哈!!!
参考文档
从一道题浅说 JavaScript 的事件循环 · Issue #61 · dwqs/blog (github.com)
前端工程师一定要懂哪些浏览器原理?-极客时间 (geekbang.org)