关于 js 执行机制,老早之前就一直想写篇文章做个总结,因为和 js 执行顺序的面试题碰到的特别多,每次碰到总是会去网上查,没有系统地总结,搞得每次碰到都是似懂非懂的感觉,这篇文章就系统的总结一下 js 执行机制。
任务队列
大家都知道 js 最大的特点就是单线程执行,这就是为什么 js 简单易学的一个重要原因,不需要考虑复杂的同步问题,但是单线程也会有一个问题,所有的任务在执行的过程中都必须等待前一个任务执行完成才能执行,这样就会带来一个效率的问题,为了解决这个问题,js 将任务分为两种:同步任务和异步任务,同步任务就是之前说后一个任务必须等待前一个任务执行完成才能执行,是在主线程上执行的,而异步任务不会直接进入主线程执行,而是进入任务队列,只有在任务队列通知异步任务可以执行时,才会被推入主线程执行。让我们来看一个更加直观的流程图:
setTimeout 和 setInterval
说到异步任务,最常见就是 setTimeout 和 setInterval 两兄弟了,setTimeout 是延迟一定时间后执行,但是只执行一次,setInterval 是每隔一定的时间执行一次,会执行多次,但是有时候我们会发现设置一定的延迟时间后,回调函数的执行时间会比我们设置的时间要晚,这是为什么呢?上面我们说过,在任务执行的时候 setTimeout 这类异步任务的回调会被放到异步队列中等待执行,当延迟时间结束时,如果主线程的任务已经执行完了,也就是处在空闲状态时,就会将任务队列的回调推到主线程执行,但是当主线程的任务还没有执行完成时,就只能继续等待,来看一个例子:
let before = new Date()
setTimeout(() => {
console.log(new Date() – before)
}, 1000)
for (let i = 0; i < 300000; i++) {
console.log(‘time delay’)
}
从上面的例子就可以看到:当我们执行完 setTimeout 之后,立刻执行 20 万次的循环,从执行结果可以看到,setTimeout 回调函数中的时间远高于设置 1000ms,这就是因为时间到了,但是主线程的任务还没有执行完成导致。这种问题在 setInterval 设置倒计时的经常遇到,倒计时开始的时候设置的时间是从服务器拿到的系统时间很准确,但是如果后面不定期像服务期请求系统时间进行校准的话,你可能会发现倒计时的偏差越来越来大,这就是主线程执行的时间比设定的延迟时间长导致的。
macrotask 和 microtask
在 js 中,异步任务除了有 setTimeout 这类的异步任务,还有一类就是 es6 中很常用 promise…then 这类的异步任务,因此除了同步任务和异步任务,任务还可以更加细分为 macrotask(宏任务) 和 microtask(微任务)macrotask: 包括 setTimeout、setInterval 和执行栈 microtask: 包括 Promise、process.nextTick 要想理解这两个概念,直接从一道简单的面试题入手,来看一个例子:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
思考一下上面例子的输出结果,我们来仔细分析一下执行过程:
第一轮:主线程开始执行,遇到 setTimeout,将 setTimeout 的回调函数丢到宏任务队列中,在往下执行 new Promise 立即执行,输出 2,then 的回调函数丢到微任务队列中,再继续执行,遇到 process.nextTick,同样将回调函数扔到为任务队列,再继续执行,输出 5,当所有宏任务执行完成后看有没有可以执行的微任务,发现有 then 函数和 nextTick 两个微任务,先执行哪个呢?process.nextTick 指定的异步任务总是发生在所有异步任务之前,因此先执行 process.nextTick 输出 4 然后执行 then 函数输出 3,第一轮执行结束。
第二轮从宏任务队列开始,发现 setTimeout 回调,输出 1 执行完毕,因此结果是 25431
最后用一张图来总结一下:
总结
这篇文章简单介绍了 js 执行机制,希望看了之后,可以对大家认识 js 的执行机制会有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞收藏