js线程执行原理
众所周知,浏览器执行JS是单线程的,当然除了最新的worker线程,worker线程号称是浏览器端执行复杂运算一种解决方案,可以与主线程并行运算,而不互相排斥。浏览器线程主要分为以下三种:
1.就是我们熟悉的js线程,也就是js主引擎线程,执行js主线程代码。由于浏览器是单线程,所以浏览器无论什么时候都是一个线程在运行,这个线程也一直等待执行任务
2.GUI线程,执行dom树和css树回流和重绘。这里引起回流和重绘原因有很多种,都是由这个线程去处理,这里要注意的是GUI和js主引擎线程是互斥的,所以通常情况下,js引擎线程执行时间不能过长,过长就会引起GUI线程等待时间过长,页面就会造成卡顿。
3.事件触发线程,用于触发注册以及事件回调和异步代码执行。我们编写的异步代码(事件触发程序和settimeout),js主引擎执行到这些代码时,会先扔给事件触发线程,事件触发线程里面建立一个队列,按照先进先出的原则,依次把异步代码扔进队列,等待主线程空闲之后,再从触发线程队列中依次取出。
4.事件循环线程,用于管理我们的异步代码,存放到一个线程池中
那我们熟悉这四个线程之后,我们就可以稍微明白js异步代码的执行原理,首先js异步代码并不是真正的异步代码,伪异步执行,只有在JS线程空闲的时候,才会去取监听线程代码程序。下面,我们阐述一下setTimeout执行原理。
比如:
`
console.log('+++++++++')
setTimeout(()=>{
console.log('执行了异步代码');
},0);
console.log('==============')
`
主线程程序,按照顺序执行,遇到setTimeout,则是交给事件触发线程,由于时间是0,所以立即进去队列,然后主线程再去执行下面的代码,所以依次打印 ++++++++, =======,等到主线程空闲了,然后从事件循环线程中取出异步代码执行程序,执行settimeout,之后再打印'执行了异步代码'。
下面我们来看下一段代码
`
function threadDemo() {
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
}, 0);
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
Promise.resolve().then(() => {
console.log('8');
});
setTimeout(() => {
console.log(6);
}, 0)
console.log(7);
}
threadDemo();
`
如果你能知道最后的运行结果,就知道后面我们要讲述的内容。
js异步任务分为macrotask和mincrotask,这两个任务都是在主线程的某个时机放入任务队列中。
宏任务Macrotask, 在浏览器端,其可以理解为该任务执行完后,在下一个macrotask执行开始前,浏览器可以进行页面渲染:
setTimeout / setInterval / setImmediate / IO / render / postMessage / script整体代码都是属于宏任务。
微任务mincrotask,可以理解为在macrotask任务执行后,页面渲染前立即执行的任务:
promise.then / process.nexttick / MutationObserver
微任务的执行优先级高于宏任务,每个宏任务执行完成后,都会清空微任务队列。
比如,我们执行一段script代码里面含有微任务,那这个执行过程是这样的。
首先,执行整体script代码,我们可以视为宏任务,执行过程中,如果遇到settimeout此类的宏任务就会把宏任务加入到任务执行队列中,遇到微任务则加入到微任务执行队列中。
然后,整体script宏任务执行完成后,就会开始执行微任务队列,如果这个过程中微任务又产生了微任务,则继续加到微任务队列的末位,直至整个微任务执行完成。
其次,微任务执行完成后,就开始UI线程的渲染。
最后,UI线程渲染完成后,js主线程又会继续到任务队列中取宏任务,重复上述步骤。
所以上述代码的执行结果是:1,4,7,5,8,2,3,6