JavaScript 运行机制详解(宏任务与微任务的解读 eventLoop)
JavaScript 运行机制详解(宏任务与微任务的解读 eventLoop)
前言
1. 为什么JavaScript是单线程?
2. 任务队列
3. 微任务,宏任务
一、为什么JavaScript是单线程?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
二、任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 | |
---|---|
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 | |
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 | |
(4)主线程不断重复上面的第三步。 |
如图展示
三,微任务,宏任务
js是单线程的,而浏览器是多线程,浏览器的event loop至少包含两个队列,macrotask 队列和队列。
微任务和宏任务都皆为异步操作,他们都是属于一个队列。主要区别在于他们的执行顺序,eventloop的走向和取值。
常见宏任务与微任务
宏任务
宏任务,macrotask ,又叫 tasks 。一些异步任务的回调会依次进入 ** macro task queur** , 等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate(Node环境下独有)
- requestAnimationFrame(浏览器环境独有)
- I/O
- UI rendering(浏览器独有)
微任务
微任务,microtask,又叫 jobs。另外一些异步任务的回调会依次进入 micro task queue,等待后续被调用,这些异步任务包括:
- process.nextTick(Node环境独有)
- Promise.then()
- Object.observe
- MutationObserver
微任务和宏任务的工作流程
js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue最骚的是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回掉函数,然后再从宏任务的queue上拿宏任务的回掉函数。 我当时看到这我就服了还有这种骚操作(记住这么骚,这么秀就好)。
4.理解理论,做这套面试题再进一步理解
// 请将下面这段代码的执行结果写出来
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')
这段代码的执行结果会是什么呢?
知识点:
考察的是事件循环和回调队列。注意以下几点:
1.Promise 优先于 setTimeout 宏任务,所以 setTimeout 回调会最后执行
2.Promise 一旦被定义就会立即执行
3.Promise 的 resolve 和 reject 是异步执行的回调。所以 resolve() 会被放到回调队列中,在主函数执行完和 setTimeout 之前调用
4.await 执行完后,会让出线程。async 标记的函数会返回一个 Promise 对象
解析:
因为settimeout是宏任务,虽然先执行的他,但是他被放到了宏任务的eventqueue里面,然后代码继续往下检查看有没有微任务,检测到Promise的then函数把他放入了微任务序列。等到主线进程的所有代码执行结束后。先从微任务queue里拿回掉函数,然后微任务queue空了后再从宏任务的queue拿函数。
1.首先,事件循环从宏任务(macrostack)队列开始,这个时候,宏任务队列中,只有一个 script (整体代码)任务。从宏任务队列中取出一个任务来执行。
2.首先执行 console.log(‘script start’),输出 ‘script start’
3.遇到 setTimeout 把 console.log(‘setTimeout’) 放到 macrotask 队列中
4.执行 aync1() 输出 ‘async1 start’ 和 ‘async2’ ,把 console.log(‘async1 end’) 放到 micro 队列中
5.执行到 promise ,输出 ‘promise1’ ,把 console.log(‘promise2’) 放到 micro 队列中
执行 console.log(‘script end’),输出 ‘script end’
6.macrotask 执行完成会执行 microtask ,把 microtask quene 里面的 microtask 全部拿出来一次性执行完,所以会输出 ‘async1 end’ 和 ‘promise2’
7.开始新一轮的事件循环,去除执行一个 macrotask 执行,所以会输出 ‘setTimeout’