产生宏任务和微任务的方式
产生宏任务的方式
- script中的代码块
- setTimeout()
- setInterval()
- setImmediate()(非标准的方法,IE和nodejs中支持)
- postMessage
- I/O
-
产生微任务的方式
new Promise().then(回调)(promise的构造函数是同步执行,promise.then中的函数是异步执行)
- MutationObserver 这个对象,可以去监听dom对象的改变,当dom改变之后,可以执行一个回调函数,这个回调函数在微任务队列中
-
关于promise的解释
promise的构造函数是同步执行,promise.then中的函数是异步执行
构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用。promise 状态一旦改变则不能再变。 promise 有 3 种状 态: pending、fulfilled 或 rejected。 状态改变只能是 pending->fulfilled 或者 pending-> rejected,状态一旦改变则不能再变。
promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。
- 如果在一个then()中没有返回一个新的promise,则return 什么下一个then就接受什么,如果then中没有return,则默认return的是 undefined.
- then()中包含.then()的嵌套情况 then()的嵌套会先将内部的then()执行完毕再继续执行外部的then();在多个then嵌套时建议将其展开,将then()放在同一级,这样代码更清晰。
- catch和then的连用 如果每一步都有可能出现错误,那么就可能出现catch后面接上then的情况
- Promise.all() 将多个Promise批量执行,所有的Promise都完毕之后返回一个新的Promise
- Promise.race() 和Promise.all()差不多,区别就是传入的数组中有一个Promise完成了则整个Promise完成了。
关于Promise的解释参考地址 promise执行顺序总结
const promise = new Promise((resolve, reject) => {
resolve('success1')
reject('error')
resolve('success2')
})
promise
.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
// 执行的then是 success1
第五条的代码
console.log('start');
new Promise((resolve,reject)=>{
setTimeout(function(){
console.log('step');
resolve(110);
},1000)
})
.then((value)=>{
return new Promise((resolve,reject)=>{
setTimeout(function(){
console.log('step1');
resolve(value);
},1000)
})
.then((value)=>{
console.log('step 1-1');
return value;
})
.then((value)=>{
console.log('step 1-2');
return value;
})
})
.then((value)=>{
console.log(value);
console.log('step 2');
})
/* start step step1 step 1-1 step 1-2 110 step 2 */
//展开之后的代码
console.log('start');
new Promise((resolve,reject)=>{
setTimeout(function(){
console.log('step');
resolve(110);
},1000)
})
.then((value)=>{
return new Promise((resolve,reject)=>{
setTimeout(function(){
console.log('step1');
resolve(value);
},1000)
})
})
.then((value)=>{
console.log('step 1-1');
return value;
})
.then((value)=>{
console.log('step 1-2');
return value;
})
.then((value)=>{
console.log(value);
console.log('step 2');
})
浏览器中的Event loop
- 只有两个任务队列,宏队列和微队列
- 每次宏任务执行完之前会先把微任务执行完毕
- 同一次事件循环中,微任务永远在宏任务之前执行。
- 微任务执行是在调用栈上的代码执行完,渲染之前执行
setTimeout(() => {
console.log('s1');
Promise.resolve().then(() => {
console.log('p1');
})
Promise.resolve().then(() => {
console.log('p2');
})
})
setTimeout(() => {
console.log('s2');
Promise.resolve().then(() => {
console.log('p3');
})
Promise.resolve().then(() => {
console.log('p4');
})
})
// 执行顺序s1 p1 p2 s2 p3 p4
浏览器中完整事件环执行顺序
下面的东西看着有点懵的话请务必看一下大佬的文章(https://segmentfault.com/a/1190000022805523#item-2)
- 从上至下执行所有的同步代码
- 执行过程中将遇到的宏任务与微任务添加至相应的任务队列
- 同步代码执行完毕后,执行满足条件的微任务回调
- 微任务队列执行完毕后执行所有满足需求的宏任务回调(先进先出)
- 循环事件环操作
- 注意:每执行一个宏任务之后就会立刻检查微任务队列
- 注意:promise传入的函数executor会在传入的同时被执行,如果里面有同步代码会被优先执行
- 连续调用then,第一个then执行完之后,会将下一个then放入队列中
- 解释:promise的构造函数是同步执行,promise.then中的函数是异步执行。
- 同步 -> 微任务 ->宏任务-微任务-宏任务-微任务
- 在Call Stack中不仅会形成宏任务队列,还会形成微任务队列,在每一次Call Stack清空后都需要将微任务队列清空(全部执行)
宏任务、微任务的执行顺序 执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
- 宏任务:DOM渲染后触发,由浏览器规定(Web APIs)
-
event loop和Dom渲染
每次Call Stack清空(即每次轮询结束),即同步任务执行完
- 都是Dom重写渲染的机会,Dom结构如有改变则重新渲染
- 然后再去触发下一次Event Loop
微任务和宏任务之间存在dom渲染
const main = document.getElementById('main');
const frg = document.createDocumentFragment();
for(let i = 0; i < 10; i++) {
const li = document.createElement('li');
li.innerHTML = i;
frg.appendChild(li);
}
main.appendChild(frg);
new Promise((resolve) => {
resolve();
}).then(() => {
console.log('微任务已经执行');
alert('dom 还未插入')
});
setTimeout(() => {
console.log('宏任务执行');
alert('dom 已经插入')
});
setTimeout(() => {
console.log('s1');
Promise.resolve().then(() => {
console.log('p2');
})
Promise.resolve().then(() => {
console.log('p3');
})
})
Promise.resolve().then(() => {
console.log('p1');
setTimeout(() => {
console.log('s2');
});
setTimeout(() => {
console.log('s3');
});
})
// 按照顺序压入宏任务和微任务队列中
// p1 s1 p2 p3 s2 s3
代码示例
executor会在传入的同时被执行,v8的实现中c的micro task被创建得更早
关于连续调用then
then1执行之后会将then2放入微任务队列,之前的微任务队列在执行完then1已经空了,但却新放入了then2,所以会执行then2,then2之后then3 。。。一直循环清空微任务队列,才会去执行宏任务
new Promise((res, rej) => {
res()
}).then((v) => { // then1
console.log(1)
return 2
}).then(v => { // then2
console.log(v)
}).then(v => { // then3
console.log(v)
})
setTimeout(() => {
console.log('setr')
}, 0);
关于async await
- async最终返回的是一个promise, await 后面的代码(指所有)相当于promise.then的代码,会添加到微任务队列中
- 如果没有改变promise的状态,那么接下来的代码也不会执行
- await后面如果跟一个promise对象,await将等待这个promise对象的resolve状态的值value,且将这个值返回给前面的变量,此时的promise对象的状态是一个pending状态,没有resolve状态值,所以什么也打印不了 ```javascript async function t2(){ let a = await new Promise((resolve) => {}) console.log(a) } t2() // 不会执行
async function t1(){ let a = await “lagou” console.log(a) } t1() // lagou
<a name="kZ0WX"></a>
##### await的执行过程<br />![await.png](https://cdn.nlark.com/yuque/0/2022/png/22628793/1653797414292-270df2b0-3827-41f9-820b-9f5d1dabdbea.png#clientId=u80143c46-7f41-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u9fddd3c1&margin=%5Bobject%20Object%5D&name=await.png&originHeight=856&originWidth=841&originalType=binary&ratio=1&rotation=0&showTitle=false&size=233563&status=done&style=none&taskId=ua8d541e3-36a9-48f9-ac64-7a7fdcc5896&title=)
<a name="DXPnw"></a>
#### 经典面试题
```javascript
async function asycn1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout ');
}, 0);
asycn1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务
从上往下执行代码,先执行同步代码,输出 script start
遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
接着往下执行,输出 promise1,把 .then()放到微任务队列中;注意Promise本身是同步的立即执行函数,.then是异步执行函数
接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
依次执行微任务中的代码,依次输出 async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout
最后的执行结果如下
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
问:请问为什么第一题的console.log('async1 end')会被放到微任务里?
因为他前面有一个await,并且这个await等待的是一个promise,所以会阻塞后面的代码执行,await相当于promise的then
大佬的面试经过
- 遍历 microtask 队列时新增了 microtask,本轮循环执行新增的 microtask,直至 microtask 队列长度为 0
- 注意审题。microtask 队列、promise 和 setTimeout 的源码我全都看过,还写过文章,最后还是答错了,要多做面试题,提高手感
- 数组的相关方法,循环次数预先确定,如果在循环过程中数组长度发生改变,循环次数不变。microtask 队列的循环则相反,循环过程中新增了 microtask,循环次数增加
NodeJs事件循环机制
- timer:执行setTimeout与setInterval回调
- pending callback:执行系统操作的回调,例如:tcp, udp
- idle、prepare:只在系统内部进行使用
- poll:执行与 I/O 相关的回调(文件读写)
- check:执行setImmediate中的回调
- close callback :执行close事件的回调
微任务:process.nextTick的执行优先级高于Promise
Nodejs中完整的事件环执行顺序
- 执行同步代码,将不同的任务添加至相应的队列
- 所有同步代码执行后会去执行满足条件微任务(proces.nextTick执行优先级高于Promise)
- 所有微任务代码执行后会执行timer队列中满足的宏任务(setTimeout与setInterval等)
- timer中的所有宏任务执行完成后就会依次切换队列
- 注意:微任务执行1. 同步执行完之后会执行满足条件的微任务,2.在完成队列切换之前会先清空微任务代码 ```javascript // 宏任务 timer setTimeout(() => { console.log(‘s1’); })
// 微任务 Promise.resolve().then(() => { console.log(‘p1’); })
// 同步任务 console.log(‘start’);
// 微任务 process.nextTick(() => { console.log(‘tick’); })
// 宏任务 check setImmediate(() => { console.log(‘setImmediate’); })
// 同步任务 console.log(‘end’);
// 执行顺序:start end tick p1 s1 setImmediate
// 事件队列执行顺序 微任务 —> timer — > poll —-> check
// 微任务中process.nextTick执行优先级高于Promise
<a name="RtINu"></a>
#### Nodejs事件环理解
```javascript
// timer
setTimeout(() => {
console.log("s1");
// 微任务
Promise.resolve().then(() => {
console.log("p1");
});
// 微任务
process.nextTick(() => {
console.log("t1");
});
});
// 微任务
Promise.resolve().then(() => {
console.log("p2");
});
console.log("start");
// timer
setTimeout(() => {
console.log("s2");
// 微任务
Promise.resolve().then(() => {
console.log("p3");
});
// 微任务
process.nextTick(() => {
console.log("t2");
});
});
console.log("end");
// strat end p2 s1 s2 t1 t2 p1 p3
NodeJs与浏览器事件环区别
- 浏览器中只有两个任务队列(宏任务和微任务)
Nodejs中有6个事件队列(timer,pending callback,idle prepare,poll,check, close callback)
微任务执行时机
二者都会在同步代码执行完后执行微任务
- 浏览器平台下每当一个宏任务执行完毕后就清空微任务
-
微任务执行优先级
浏览器事件环中,微任务存放事件队列执行顺序,先进先出
Nodejs中 process.nextTick执行优先于promise.then
Nodejs事件环常见问题
执行顺序不固定 ```javascript // timer如果有延迟,会继续向下走,执行check里面的代码,这就造成了输出不一致 setTimeout(() => { console.log(‘timerout’); },0);
// check // 不需要设置时间,没有延迟 setImmediate(() => { console.log(‘immediate’); })
// node中, timer poll check
- readFile在poll里面
- 放在了IO回调当中,setImmediate的回调和setTimeout中的回调顺序就固定了
```javascript
const fs = require("fs");
// poll
fs.readFile("/m1.js", () => {
// timer
setTimeout(() => {
console.log("timeout"); // 后执行
}, 0);
// check
setImmediate(() => {
console.log("setImmediate"); // 先执行
});
});
// readFile存放在poll里面,接着向下执行,执行check里面的回调,然后才会去timer里面
推荐文章
浅析setTimeout与Promise
JavaScript宏任务和微任务、事件循环及经典案例
关于promise的解释
强烈推荐 JavaScript中的Event Loop(事件循环)机制
microtask 队列与 async/await 源码分析
Promise V8 源码分析(一)