我们在写代码的时候总是会遇到一些问题:
- 为什么
setTimeout会比Promise后执行,明明代码写在Promise之前?
console.log('start')setTimeout(()=>{console.log('setTimeout')},0)Promise.resolve().then(()=>{console.log('promise')})console.log('end');
结果:
- 为什么用setTimeout更新dom渲染的时候总会闪烁下,而用Promise时候就会无感知的变化?
let app = document.getElementById("app");app.innerHTML = '1111';// 使用setTimeout更新DOMsetTimeout(()=>{app.innerHTML = '2222' //能看到1111 -> 2222 变化闪烁},0)// 使用 Promise更新DOM(new Promise((resolve) =>{resolve();})).then(()=>{app.innerHTML = '3333';// 看不到闪烁 直接 3333})
如果你了解消息队列和事件循环这些知识,上面的出现的‘神奇’现象将变得没那么‘神奇’,so easy!!!
一. 浏览器中的事件循环
首先我们来看看浏览器中的事件循环:
通过图片可以看到浏览器的事件循环机制。在讲解流程的之前。我们先来了解下一些概念。
1. 执行上下文栈(调用栈,执行栈)
js代码执行过程中用来管理上下文的栈
在 JavaScript 代码运行过程中,会进入到不同的执行环境中,一开始执行时最先进入到全局环境,此时全局上下文首先被创建并入栈,之后当调用函数时则进入相应的函数环境,此时相应函数上下文被创建并入栈,当处于栈顶的执行上下文代码执行完毕后,则会将其出栈。这里的栈便是执行上下文栈。
eg:
function fn2() {console.log('fn2')}function fn1() {console.log('fn1')fn2();}fn1();
对于上面代码中的执行上下文栈变化行为如下图

大家也知道了当我们执行 JS 代码的时候其实就是往执行上下文栈中放入函数,那么遇到异步代码的时候该怎么办?
2. 任务队列
其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈执行完一个任务,事件循环 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
在 JavaScript 事件循环机制中,存在多种任务队列,其分为宏任务(macro-task)和微任务(micor-task)两种。
- 宏任务:当前调用栈中执行的代码成为宏任务。(主代码快,定时器等等)。
- 微任务: 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件。(promise.then,proness.nextTick等等)。
- 宏任务中的事件放在callback queue中,由事件触发线程维护;微任务的事件放在微任务队列中,由js引擎线程维护。
- 宏任务包括:setTimeout、setInterval、I/O、UI rendering
- 微任务包括:Promise、Object.observe(已废弃)、MutationObserver(html5新特性)
当然不同的任务源对应的任务队列其执行顺序优先级是不同
- 宏任务队列中,各个队列的优先级为
setTimeout > setInterval > I/O
- 在微任务队列中,各个队列的优先级为
Promise > Object.observe > MutationObserver
3. 事件循环机制流程
我们知道这么多概念下面我们来梳理下事件循环机制流程:
- 在执行栈中执行一个宏任务。 当遇到各种任务源时将其所指定的异步任务挂起,接受到响应结果后将异步任务放入对应的任务队列中
- 执行过程中遇到微任务,将微任务添加到微任务队列中。
- 当前宏任务执行完毕,立即执行微任务队列中的任务。
- 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染。
- 渲染完毕后,js线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取)。
正好对应着之前的图片
下面我们举个栗子来跑一遍流程:
console.log('global');setTimeout(function() {console.log('setTimeout1');new Promise(function(resolve) {console.log('setTimeout1_promise');resolve();}).then(function() {console.log('setTimeout1_promiseThen')})process.nextTick(function() {console.log('setTimeout1_nextTick');})},0)new Promise(function(resolve) {console.log('promise1');resolve();}).then(function() {console.log('promiseThen1')})setImmediate(function() {console.log('setImmediate');})process.nextTick(function() {console.log('nextTick');})new Promise(function(resolve) {console.log('promise2');resolve();}).then(function() {console.log('promiseThen2')})setTimeout(function() {console.log('setTimeout2');},0)
一,执行 Javascript 代码,全局上下文入栈,输出 global ,此时遇到第一个 setTimeout 任务源,由于其执行延迟时间为 0,所以能够立即接收到响应结果,将其指定的异步任务放入宏任务队列中;
二,遇到第一个 Promise 任务源,此时会执行 Promise 第一个参数中的代码,即输出 promise1,然后将其指定的异步任务(then 中函数)放入微任务队列中;
三,遇到 setImmediate 任务源,将其指定的异步任务放入宏任务队列中;
四,遇到 nextTick 任务源,将其指定的异步任务放入微任务队列中;
五,遇到第二个 Promise 任务源,输出 promise2,将其指定的异步任务放入微任务队列中;
六,遇到第二个 setTimeout 任务源,将其指定的异步任务放入宏任务队列中;
七,JavaScript 整体代码执行完毕,开始清空微任务队列,将微任务队列中的所有任务队列按优先级、单个任务队列的异步任务按先进先出的方式入栈并执行。此时我们可以看到微任务队列中存在 Promise 和 nextTick 队列,nextTick 队列优先级比较高,取出 nextTick 异步任务入栈执行,输出 nextTick;
八,取出 Promise1 异步任务入栈执行,输出 promiseThen1;
九,取出 Promise2 异步任务入栈执行,输出 promiseThen2;
十,微任务队列清空完毕,执行宏任务队列,将宏任务队列中优先级最高的任务队列中的异步任务按先进先出的方式入栈并执行。此时我们可以看到宏任务队列中存在 setTimeout 和 setImmediate 队列,setTimeout 队列优先级比较高,取出 setTimeout1 异步任务入栈执行,输出 setTimeout1,遇到 Promise 和 nextTick 任务源,输出 setTimeout1_promise,将其指定的异步任务放入微任务队列中;
十一,取出 setTimeout2 异步任务入栈执行,输出 setTimeout2;
十二,至此一个微任务宏任务事件循环完毕,开始下一轮循环。从微任务队列中的 nextTick 队列取出 setTimeout1_nextTick 异步任务入栈执行,输出 setTimeout1_nextTick;
十三,从微任务队列中的 Promise 队列取出 setTimeout1_promise 异步任务入栈执行,输出 setTimeout1_promiseThen;
十四,从宏任务队列中的 setImmediate 队列取出 setImmediate 异步任务入栈执行,输出 setImmediate;
十五,全局上下文出栈,代码执行完毕。最终输出结果为
globalpromise1promise2nextTickpromiseThen1promiseThen2setTimeout1setTimeout1_promisesetTimeout2setTimeout1_nextTicksetTimeout1_promiseThensetImmediate
