基本原理

js是单线程的,避免了多个线程同时操作同一个DOM产生矛盾的问题。但浏览器

Js引擎执行一段代码块,会将同步任务放到主线程排队执行,异步任务放到任务队列,异步任务又分宏任务和微任务,比如常见的setTimeout / setInterval 为宏任务,promise.then/Mutation Observe为微任务(在这里需要注意的是,尽管promise.then是微任务,但是其注册是在状态确认后同步的,具体后面通过示例讲解)。当主线程执行完毕后会去检查微任务队列,微任务队列中所有的任务都会被依次取出来执行,直至为空,然后再检查宏任务队列(注意一个宏任务里面的微任务执行完才会执行下一个宏任务),这个过程是循环不断的,这种运行机制称为Event Loop(事件循环)

在事件循环中,每进行一次循环操作称为tick
彻底搞通Js中的事件循环-编程知识网

宏任务微任务示例

console.log(1);setTimeout(() => {console.log(2);Promise.resolve().then(() => {console.log(3)});
});new Promise((resolve, reject) => {console.log(4)// 排到宏任务队列setTimeout(()=>resolve(5)) console.log(‘attach’)
}).then((data) => {console.log(data);
})setTimeout(() => {console.log(6);
})console.log(7);

过程:
执行打印1
放入宏任务setTimeout
执行new Promise,打印4,放入宏任务setTimeout,打印attach
放入宏任务setTimeout
执行打印7

此时打印了1,4,attach,7,
微任务队列:无
宏任务队列:依次排列的三个宏任务setTimeout

第一个tick完成

执行第一个宏任务:
打印2
将then中的回调函数放入微任务
再执行微任务队列,打印3

第二个tick完成

执行第二个宏任务:
promise resolve
将then中的回调函数放入微任务
再执行微任务队列,打印5

第三个tick完成

执行第三个宏任务:
打印6

第四个tick完成

tips

需要注意:then的回调监听最新Promise对象的resolve执行后才会注册进微任务队列,之后的then回调都依赖于前一个then中的代码执行结束。

then执行顺序的问题

请看以下题目:

new Promise((resolve,reject)=>{console.log("promise1",1)resolve()
}).then(()=>{console.log("then11",2)new Promise((resolve,reject)=>{console.log("promise2",3)resolve();}).then(()=>{console.log("then21",4)new Promise((resolve,reject)=>{console.log("promise3",5)resolve();}).then(()=>{console.log("then31",7)}).then(()=>{console.log("then32",9)})}).then(()=>{console.log("then22",8)})
}).then(()=>{console.log("then12",6)
})

输出123456789,需要注意的点:then的回调监听最新Promise对象的resolve执行后才会注册进微任务队列,之后的then回调都依赖于前一个then中的代码执行结束。
来看下面一个例子:

new Promise((resolve,reject)=>{console.log("promise1",1)resolve()
}).then(()=>{console.log("then11",2)**return** new Promise((resolve,reject)=>{console.log("promise2",3)resolve();}).then(()=>{console.log("then21",4)new Promise((resolve,reject)=>{console.log("promise3",5)resolve();}).then(()=>{console.log("then31",7)}).then(()=>{console.log("then32",9)})}).then(()=>{console.log("then22",8)})
}).then(()=>{console.log("then12",6)
})

结果是:123457896,在上题的基础上,新增了一个return。
再看一个巩固一下:

new Promise((resolve,reject)=>{console.log("promise1",1)resolve()
}).then(()=>{console.log("then11",2)return new Promise((resolve,reject)=>{console.log("promise2",3)resolve();}).then(()=>{console.log("then21",4)return new Promise((resolve,reject)=>{console.log("promise3",5)resolve();}).then(()=>{console.log("then31",7)}).then(()=>{console.log("then32",9)})}).then(()=>{console.log("then22",8)})
}).then(()=>{console.log("then12",6)
})

加入async await

只需要记住一点:每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。

具体请参考:https://blog.csdn.net/roamingcode/article/details/85052590

SetTimeout/SetInterval时间不准的问题

setTimeout时间不准

我们添加一个定时器后,在间隔我们设定时间后,将函数代码放到宏任务队列中,等主线程及微任务队列空闲时,再去执行宏任务队列。如果主线程及微任务队列和宏任务队列前面有很多耗时任务,那么就会延迟执行定时器任务。

SetInterval时间不准

对于setInterval定时器代码规则地插入队列中(仅当没有该定时器的其他代码时,才将定时器代码添加到队列中,避免了代码的连续执行)。如果定时器代码或者任务队列里面有耗时任务,执行时间超过了设定的时间,那么下一次的代码得等到当前执行完后再立即执行,从而造成不准时。

解决办法:
递归调用setTimeout来实现setInterval,这样就能保证至少间隔准时。