今天的主题或许让你感到害怕。别慌,这篇文章也许就是你想要的。
首先来看一个眼熟的面试题:
console.log(1)
let promise = new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(data){
console.log(data)
})
setTimeout(function(){
console.log(4);
})
console.log(5)
// 上面代码的运行结果是 1 2 5 3 4
一开始整个脚本作为一个宏任务执行。
执行过程中同步代码直接执行,宏任务等待时间到达或者成功后,将方法的回调放入宏任务队列中,微任务进入微任务队列。
当前主线程的宏任务执行完出队,检查并清空微任务队列。
然后再取出一个宏任务执行。以此循环…
乍一看这个概念感觉有点绕,我们通过例子🌰来说明:
首先看一下栈的执行方式。
var a = "hello";
function one(){
let a = 1;
two();
function two(){
let b = 2;
three();
function three(){
console.log(b)
}
}
}
console.log(a);
one();
// 上面代码的运行结果是 hello 2
执行栈里面最先放的是全局作用域(代码执行有一个全局文本的环境),然后再放one, one 执行再把 two 放进来,two 执行再把 three 放进来,一层叠一层。
最先走的肯定是 three,因为 two 要是先销毁了,那 three 的代码 b 就拿不到了,所以是先进后出(先进的后出),所以,three 最先出,然后是 two 出,再是 one 出。
那队列又是怎么一回事呢?
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3);
})
setTimeout(function(){
console.log(4);
})
console.log(5);
// 首先执行了栈里的代码,1 2 5。前面说到的 settimeout 会被放在队列里,当栈执行完了之后,从队列里添加到栈里执行(此时是依次执行),得到 3 4
再来看一个例子🌰:
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3);
setTimeout(function(){
console.log(6);
})
})
setTimeout(function(){
console.log(4);
setTimeout(function(){
console.log(7);
})
})
console.log(5)
// 同样,先执行栈里的同步代码 1 2 5. 再同样,最外层的settimeout会放在队列里,当栈里面执行完成以后,放在栈中执行,3 4。而嵌套的2个settimeout,会放在一个新的队列中,去执行 6 7.
把上面的例子🌰变化一下:
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3);
setTimeout(function(){
console.log(6);
})
},400)
setTimeout(function(){
console.log(4);
setTimeout(function(){
console.log(7);
})
},100)
console.log(5)
// 如上:这里的顺序是1,2,5,4,7,3,6。也就是只要两个set时间不一样的时候 ,就set时间短的先走完,包括set里面的回调函数,再走set时间慢的。(因为只有当时间到了的时候,才会把set放到队列里面去)
所以可以得到结论,永远都是栈里的代码先行执行,再从队列中依次读事件,加入栈中执行。
stack(栈)里面都走完之后,就会依次读取任务队列,将队列中的事件放到执行栈中依次执行。这个时候栈中又出现了事件,这个事件又去调用了 WebAPIs 里的异步方法,那这些异步方法会在再被调用的时候放在队列里,然后这个主线程(也就是 stack)执行完后又将从任务队列中依次读取事件,这个过程是不断循环的。
我们再回到第一个例子🌰:
console.log(1)let promise = new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(data){
console.log(data)
})
setTimeout(function(){
console.log(4);
})console.log(5)
上面代码的运行结果是 1 2 5 3 4
为什么 setTimeout 要在 Promise.then 之后执行?
为什么 new Promise又在 console.log(2) 之前执行?
setTimeout 是宏任务,而 Promise.then 是微任务。这里的 new Promise() 是同步的,所以是立即执行的。
宏任务和微任务
宏任务
浏览器为了保证 JS 内部宏任务与 DOM 任务能够有序地执行,会在一个 task 执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->…)。鼠标点击会触发一个事件回调,需要执行一个宏任务,然后解析 HTML。
微任务
通常来说,微任务就是需要在当前 task 执行结束后立即执行的任务。比如对一系列动作做出反馈,或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的 JS 代码正在执行且每个宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。微任务包括了 mutation observe 的回调还有接下来的例子 promise 的回调。
同样地,来看例子🌰。
console.log('1');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('2');
// 运行结果:1 2 promise1 promise2 setTimeout
setTimeout 的作用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为什么打印 ‘setTimeout’ 在 ‘promise1 ,promise2’ 之后。因为打印 ‘promise1 , promise2’ 是第一个宏任务里面的事情,而 ‘setTimeout’ 是另一个新的独立的任务里面打印的。
最后来总结回顾一下:
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行。
- 主线程从任务队列中读取事件,这个过程是循环不断的。