一秒就懂 | 聊聊 JS 宏任务和微任务

今天的主题或许让你感到害怕。别慌,这篇文章也许就是你想要的。

首先来看一个眼熟的面试题:

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’ 是另一个新的独立的任务里面打印的。

最后来总结回顾一下:

  • 所有同步任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行。
  • 主线程从任务队列中读取事件,这个过程是循环不断的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值