函数调用栈和消息队列
- 函数调用栈
我们知道,js是
单线程
的,也就是说只有一个线程在执行js的代码。当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入调用栈
。函数执行完毕后,该函数就会在调用栈中被清除,然后函数调用栈继续:进入下一个函数 => 执行该函数 => 弹出该函数 这样一个往复的流程。
- 消息队列
消息队列又叫 任务队列,是一个有序的
异步任务集合列表
,当代码里有异步操作的时候,它们不需要立刻被执行,当代码执行到他们的时候,并不会立刻把它们压入函数调用栈 ,而是先放到消息队列中等待,等到消息队列中的任务执行完毕后,才按照一定的规则(Event-Loop)将消息队列里的函数推入到函数调用栈。消息队列分为两种:宏任务(macro-task)
队列和微任务(micro-task)
队列。任务的读取方式为先进先出,后进后出
(存在例外,如 process.nextTick等)。
常见的宏任务包括:1、DOM操作 2、用户交互(事件) 3、网络任务(ajax)4、定时器:setTimeOut、setImmediate
。
微任务指的是ES6之后和 Promise 相关的所有语法:Promise、async/await等等, process.nextTick 在微任务中优先级最高
。
Event-Loop
JS是
单线程
的语言,所有的异步都要通过异步
实现,而Event-Loop
就是JavaScript的异步实现机制。
一道Event-Loop题目
setTimeout(() => { //9
console.log('immediate')
})
async function async1() {
console.log('async1 start') // 同步 2
await async2() // 同步
console.log('async1 end') // 7 await 后面的内容,异步(微任务),相当于 callback 函数里的内容
}
async function async2() {
console.log('async2') // 3
}
console.log('script start') //1
setTimeout(function () {
console.log('setTimeout') //10
}, 0)
async1() // 同步
new Promise(function (resolve) {
console.log('promise1') // 同步 4
resolve()
}).then(function () {
console.log('promise2') // 8
})
console.log('script end') // 同步完成 5
上述的打印入注释所示,结果如下图所示,那么为什么是按照这个顺序打印呢,这涉及到Event-Loop的实现机制。
图解 Event-Loop 渲染机制
- 先把
微任务
队列里的任务拿出来交给全局执行上下文执行- 再把
宏任务
队列里的任务拿出来交给全局执行上下文执行- 不断的循环
以上为 Event-Loop 的基本执行规则,以文章开头题目为例进行讲解
setTimeout(() => { // 9
console.log('immediate')
})
async function async1() {
console.log('async1 start') // 同步 2
await async2() // 同步
console.log('async1 end') // 7 await 后面的内容,异步(微任务),相当于 callback 函数里的内容
}
async function async2() {
console.log('async2') // 3
}
console.log('script start') //1
setTimeout(function () {
console.log('setTimeout') //10
}, 0)
async1() // 同步
new Promise(function (resolve) {
console.log('promise1') // 同步 4
resolve()
}).then(function () {
console.log('promise2') // 8
})
console.log('script end') // 同步完成 5
1 代码开始运行,此时函数调用栈获取全局上下文。 JS线程首先执行到定时器: setTimeout(() => { console.log(‘immediate’) }), 它是个宏任务,被推到宏任务队列:
2 随后代码执行至:console.log(‘script start’),它不属于异步任务,会立刻被推入函数调用栈,立刻执行
3 代码继续执行:setTimeout(function () { console.log(‘setTimeout’) //10 }, 0),它也是个宏任务,被推到宏任务队列
4 随后代码执行至:async1(),进入async1后,它会相继执行以下代码:
console.log('async1 start')
await async2()
console.log('async1 end')
在前一篇文章中提到过,async 之后的代码并不是微任务, await相当于 promise 中的 then 方法,它会把后续的任务会变为微任务执行,因此,console.log(‘async1 start’) 和 async2() 作为同步任务会立刻被打印, console.log(‘async1 end’) 作为异步微任务则被推入微任务队列。
await fn() 类似于 const a = fn() ,fn的执行先于a的声明
5 随后代码执行至 new Promise 部分,console.log(‘promise1’) 作为同步任务,立刻进入函数调用栈被执行,then方法中的任务作为异步被推送至微任务队列
6 最后,执行至console.log(‘script end’) 作为同步任务,立刻进入函数调用栈被执行。 至此,同步任务已经全部执行完毕,异步任务分别被推入对应的消息队列。
7 同步任务全部执行完毕后函数调用栈被清空,微任务队列中的任务依次进栈被调用。
8 宏任务全部执行完毕后函数调用栈被清空,微任务队列中的任务依次进栈被调用。
至此,event-loop执行完毕
DOM渲染时机
Javascript 和DOM渲染在浏览器内核中共用
一个线程
,因为 Javascript 可修改DOM结构。浏览器的实际执行应该是:
先把函数调用栈里的同步任务拿出来交给全局执行上下文执行
再把微任务队列里的微任务拿出来交给全局执行上下文执行
执行DOM渲染
把宏任务队列里的宏任务拿出来交给全局执行上下文执行
重复上述过程
总结
函数调用栈和消息队列
Event-Loop
- 一道Event-Loop题目
- 图解 Event-Loop 渲染机制
DOM渲染时机