JavaScript 事件循环宏任务微任务
十七喜欢前端 人气:0前言
相信对于刚学习JavaScript的新手来说,去理解JS中的事件循环原理以及异步执行过程比较困难,但是这是JS必须要会的基础知识,逃避不能解决问题,笔者曾经也被这个知识点困扰过,现根据以往的经验编写此文章,旨在帮助大家彻底搞懂它们以及自我巩固,话不多说,进入正题。
注意:本篇文章主要是基于浏览器环境,Node环境没有研究过暂不讨论
引言
我们先来小试牛刀,看看下面这段代码是怎么执行的,例1:
setTimeout(() => { console.log('time') }); new Promise((resolve, reject) => { console.log('p1'); resolve(); }).then(() => { console.log('res') }); console.log(1); // 输出: p1 1 res time
怎么样?你想的输出结果和实际的输出结果是一样的吗?如果是一样的说明你对事件循环有一定的了解,但是你真的已经清楚的知道了事件循环的原理吗?让我们继续往下看。
为什么会有事件循环?
JS是单线程的
众所周知:JavaScript 是一门单线程语言,也就是说,同一个时间只能做一件事。这是因为 Javascript 这门脚 本语言诞生的使命所致——JavaScript 是为处理页面中用户的交互,以及操作 DOM 而诞生的。比如我们对 某个 DOM 元素进行添加和删除操作,不能同时进行。 应该先进行添加,之后再删除。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。这样所导致的问题是: 如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉
为了解决这个问题,利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许JavaScript 脚本创建多个线程,但是子线程完全受主线程控制。于是,JS 中出现了同步任务和异步任务。
同步任务和异步任务
- 同步任务:
同步任务都在主线程上执行,形成一个执行栈。在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务:
不进入主线程、而进入”任务队列”的任务,当主线程中的任务运行完了,才会从”任务队列”取出异步任务放入主线程执行。JS 的异步是通过回调函数实现的。异步任务相关回调函数添加到任务队列中(任务队列也称为消息队列)
注意:异步任务执行机制在这里描述的比较笼统,主要方便大家理解,具体细节在后面的“宏任务与微任务”中会详细介绍
JS的事件循环就是基于同步任务与异步任务来展开的,让我们继续往下看:
JS事件循环
事件循环是JavaScript实现异步的一种方法,也是JavaScript的执行机制
如图:
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。
当遇到异步任务时不会一直等待事件的返回结果,而是将事件挂起(即交给其他线程处理,上图是指Web Worker),继续执行执行栈中的其他任务。
当异步事件返回结果时,js将异步事件callback函数放入队列中,被放入队列中的异步事件不会立即回调,等到当前执行栈中的任务都执行完成,处于闲置状态的主线程按照队列顺序将处于首位事件的callback函数放入执行栈中,执行该函数的同步代码,如果遇到了异步事件,同样也会将其回调函数放入事件队列中…
如此反复,就形成了一个循环,这也是被称为“事件循环(EventLoop)”的原因。
js事件循环的基本原理已经描述清楚,但是异步任务之间也有所不同:
任务队列实际上分为两个:宏任务队列和微任务队列。上图只表示了一个是为了便于大家理解事件循环,下面就是事件循环更细节的东西了
宏任务与微任务
上面讲到,js在执行异步任务时,回调函数会被放在js的任务队列中,实际上,回调函数的类别不同,执行的优先级也不同。
不同的优先级被分为两类,一类是宏任务(Micro task),一类是微任务(Macro task)。
回调函数是微任务时,会被放在微任务队列,回调函数是宏任务时,会被放在宏任务队列。
微任务的优先级高于宏任务,当主线程的任务执行完成时,会首先去执行微任务队列中首位的回调函数,当微任务队列中为空时,才回去执行宏任务队列中的回调函数。
常见的宏任务有哪些?
- 包括整体代码 script
- setTimeout()
- setInterval()
- setImmediate()(Node独有)
- I/O
- UI 交互事件(浏览器独有)
- requestAnimationFrame() (浏览器独有)
常见的微任务有哪些?
- Promise.then(); Promise.cath()
- async/await
- process.nextTick() (Node独有)
- MutationObserver() (H5新增,监听DOM树变化)
- Object.observe() (异步监视对象修改,已废弃)
注意:new Promise()属于同步任务,但是Promise.then(); Promise.cath()属于异步任务的微任务
执行过程总结(重点)
现在我们对事件循环有了深入了解了,但是它们的执行过程还不是很清晰,我们再把执行过程弄清楚了以后就能游刃有余了。
同步任务 —> 微任务 —> 宏任务...
- 先执行所有同步任务,碰到异步任务放到任务队列中
- 同步任务执行完毕,开始执行当前所有的异步任务
- 先执行任务队列里面所有的微任务,如果执行过程中又产生了微任务也会在本次执行过程中执行(即在下一个宏任务执行之前执行,可以看看案例1)
- 然后执行一个宏任务(从宏任务队列头部pop出一个宏任务进执行栈,该任务中的具体代码也如步骤1执行)
- 然后再执行所有的微任务(此时的微任务一般为步骤4中产生出的微任务)
- 再执行一个宏任务,再执行所有的微任务·······依次类推到执行结束。
3-6的这个循环称为事件循环Event Loop
案例挑战
学会了吗?让我们来做几个案例巩固一下吧
案例1:
const promise = new Promise((resolve, reject) => { resolve("10") }).then(res => { console.log("res1:", res) //res1: hahaha return 9 }).then(res => { console.log("res2:", res) //res2: 9 return 8 }).then(res => { console.log("res3:", res) //res3: 8 let promise2=new Promise((resolve,reject)=>{ resolve("p2") }).then(res=>{ console.log(res) setTimeout(function(){ console.log("setTimeout2") },0) }) }) console.log('aaa') setTimeout(function(){ console.log("setTimeout1") },0) const promise1 = new Promise((resolve, reject) => { console.log("p1") resolve(989) }).then(res => { console.log(res) return 990 }).then(res=>{ console.log(res) return 991 }).then(res=>{ console.log(res) return 0 }) /*输出结果: aaa p1 res1: 10 989 res2: 9 990 res3: 8 991 p2 setTimeout1 setTimeout2 */
案例2:
console.log('1'); // 定义注解 setTimeout_1 用于下文使用方便 setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) // setTimeout_2 setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) // 输出结果: 1 7 6 8 2 4 3 5 9 11 10 12
案例3:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) // 输出结果:1 7 6 8 2 4 3 5 9 11 10 12
加载全部内容