• JavaScript中宏任务和微任务执行顺序详解
  • 发布于 2个月前
  • 304 热度
    0 评论
但如果平常用不到,我们也没必要真的去看底层的代码,即使不了解底层代码,我们也可以根据具体的表现来自己定一些规则进行理解,只要根据这个规则来判断执行顺序是正确的,能指导平常开发也就足够了。这篇文章只讲基本的概念,不进行深入,能够判断 setTimemout、Promise 的执行顺序即可。

众所周知,JavaScript 单线程执行的,所以对于一些耗时的任务,我们可以将其丢入任务队列当中,这样一来,也就不会阻碍其他同步代码的执行。等到异步任务完成之后,再去进行相关逻辑的操作。

js 在主线程中执行的顺序:宏任务 -> 宏任务 -> 宏任务 ...

在每一个宏任务中又可以产生微任务,当微任务全部执行结束后执行下一个宏任务。 【宏任务 [微任务]】 -> 【宏任务 [微任务]】-> 【宏任务 [微任务]】...

宏任务
生成方法:
用户交互:用户在页面上进行交互操作(例如点击、滚动、输入等),会触发浏览器产生宏任务来响应用户操作。
网络请求:当浏览器发起网络请求(例如通过 Ajax、Fetch、WebSocket 等方式)时,会产生宏任务来处理请求和响应。
定时器:通过 JavaScript 宿主环境提供的定时器函数(例如 setTimeout、setInterval)可以设置一定的时间后产生宏任务执行对应的回调函数。
DOM 变化:当 DOM 元素发生变化时(例如节点的添加、删除、属性的修改等),会产生宏任务来更新页面。
跨窗口通信:在浏览器中,跨窗口通信(例如通过 postMessage 实现)会产生宏任务来处理通信消息。
JavaScript 脚本执行事件;比如页面引入的 script 就是一个宏任务。
重点来看下 setTimeout 。
setTimeout(() => {
    console.log('setTimeout block')
}, 100)

while (true) {

}
console.log('end here')
以上代码会输出什么?什么都不会输出上边代码相当于两个宏任务:
第一个宏任务就是上边的整个脚本
第二个宏任务是 setTimeout 传入的这个函数
() => {
    console.log('setTimeout block')
},
第一个宏任务执行到 while true 的时候死循环了,所以自己的 console.log('end here') 不会执行。
第二个宏任务也没有机会执行到。
因此什么都不会输出。
再来看一个:
const t1 = new Date()
setTimeout(() => {
    const t3 = new Date()
    console.log('setTimeout block')
    console.log('t3 - t1 =', t3 - t1)
}, 100)


let t2 = new Date()

while (t2 - t1 < 200) {
    t2 = new Date()
}

console.log('end here')
t1 记录开始的时间,设置一个 100 毫秒执行的定时器,定时器中输出执行当前任务的时间。
那么  console.log('t3 - t1 =', t3 - t1) 输出的是多少呢?
输出答案是 200。
同样的,上边是两个宏任务。
整个脚本是第一个宏任务。
计时器生成了第二个宏任务。
只有第一个宏任务执行结束后才会执行第二个宏任务。所以即使定时器时间到了也不会立刻执行,只有当第一个宏任务执行结束后才会去执行定时器的任务,此时已经过去了 200 毫秒。

微任务
生成方法:
Promise:Promise 是一种异步编程的解决方案,它可以将异步操作封装成一个 Promise 对象,通过 then 方法注册回调函数,当 promise 变为 resolve 或者 reject 会将回调函数加入微任务队列中。
MutationObserver:MutationObserver 是一种可以观察 DOM 变化的 API,通过监听 DOM 变化事件并注册回调函数,将回调函数加入微任务队列中。
process.nextTick:process.nextTick 是 Node.js 中的一个 API,它可以将一个回调函数加入微任务队列中。
重点看 Promise 的使用,关于 Promise 怎么用这里不细说了,重点放到输出顺序上。
const r = new Promise(function(resolve, reject){
    console.log("1");
    resolve()
});
r.then(() => console.log("2"));
console.log("3")
上边的输出什么:
比较基础的使用。输出 1 3 2 。
new Promise 接受一个函数,返回一个 Promise 对象。值得注意的一点是传给 Promise 的那个函数会直接执行。所以会先输出 1 。Promise 对象拥有一个 then 方法来注册回调函数,当 promise reslove 或者 reject 后会将注册函数加到微任务队列。上边的代码因为是直接 resolve 了,所以会将 () => console.log("2")  注册到微任务队列中。宏任务执行完毕后开始执行微任务,所以最后输出  2 。
再看下 async 和 await :
//堆代码 duidaima.com
async function method() {
  await method2();
  console.log(1)
}

function method2() {
  const promise = new Promise((resolve) => resolve());
  return promise;
}

function main() {
  method()
  console.log(2)
}
上边的会输出什么呢?
先输出 2,再输出 1。
这里需要明确一点,async 修饰的函数,相当于给当前函数包了一层 Promise。
所以
function main() {
  method()
  console.log(2)
}
相当于
function main() {
  new Promise((resolve,reject){ resolve(method())}
  console.log(2)
}
结合前边说的传给 Promise 的那个函数会直接执行。所以先执行 resolve(method()),进入 method 内部: 接下来是 await 的作用:遇到 await 会先执行 await 右边的逻辑,执行完之后会暂停到这里。跳出当前函数去执行之前的代码。 所以 method() 方法中,
async function method() {
  await method2();
  console.log(1)
}

function method2() {
  const promise = new Promise((resolve) => resolve());
  return promise;
}
先执行了 method2 ,当 method2 返回了 Promise 后就会暂定执行,跳回 main 函数。
function main() {
  new Promise((resolve,reject){ resolve(method())}
  console.log(2)
}
main 函数执行完毕后才会再回到 method 方法中。所以先输出 2,后输出 1。如果想要先输出 1 再输出 2 需要怎么改呢?
// 堆代码 duidaima.com
async function method() {
  await method2();
  console.log(1)
}

function method2() {
  const promise = new Promise((resolve) => resolve());
  return promise;
}

async function  main() {
  await method() // 这里 await 即可
  console.log(2)
}

main()
再看一个:
async function method() {
  new Promise((resolve) => resolve()).then(() => console.log(1));
  const n = await method2();
  console.log(n);
}

function method2() {
  const promise = new Promise((resolve) => resolve(2));
  return promise;
}

function main() {
  method();
  console.log(3);
}

main();
上边的会输出什么呢?当 main 函数执行结束后,按照之前说的应该是回到 await 那里,所以应该输出 3 2 1 吗?其实是不对的,await 还有一个特性,它会把后边执行的代码整个注册为回调函数,相当于放到了 .then 里边,如果 Promise 直接 resolve,相当于将后边的代码放到了微任务队列中。

所以
async function method() {
  new Promise((resolve) => resolve()).then(() => console.log(1));
  const n = await method2();
  console.log(n);
}
等价于:
async function method() {
  new Promise((resolve) => resolve()).then(() => console.log(1));
  new Promise((resolve) => resolve(method2())).then((n) => console.log(n));
}
在 await 之前已经有一个 Promise 把任务加到了微任务队列中。所以正确的输出顺序是 3 1 2。所以回到 await 继续执行其实是表象,本质上是从微任务队列中把之前要执行的代码取了出来继续执行。如果想输出 3 2 1 ,该怎么改代码呢?可以将 new Promise((resolve) => resolve()).then(() => console.log(1)); 这句中的 reslove() 函数延迟调用,通过 setTimeout 放到下一个宏任务中执行。
async function method() {
  new Promise((resolve) => setTimeout(resolve, 0)).then(() => console.log(1));
  const n = await method2();
  console.log(n);
}

function method2() {
  const promise = new Promise((resolve) => resolve(2));
  return promise;
}

function main() {
  method();
  console.log(3);
}
综合
如果理解了上边的,下边的内容就简单了,首先明确几个点:
1.【宏任务 [微任务]】 -> 【宏任务 [微任务]】-> 【宏任务 [微任务]】...
2.当宏任务和当前宏任务产生的微任务全部执行完毕后,才会执行下一个宏任务。每遇到生成的微任务就放到微任务队列中,当前宏任 务代码全部执行后开始执行微任务队列中的任务
3.传给 new Promise 的函数会直接执行
4.async 包装的函数相当于包了一层 Promise ,因此返回的一定是一个 Promise
5.执行到 await,先执行 await 右边的东西,执行完后后会暂停在 await 这里,并且把后边的内容丢到 then 中(再结合第 5 点)。跳到外边接着执行。外边都执行完之后开始执行微任务队列
6.当 promise 变为 resolve 或者 reject 的时候才会将 then 中注册的回调函数加入微任务队列中
7.setTimeout 产生宏任务
用户评论