JS 是单线程的。
首先,众所周知,JS 是单线程的,为什么这种低效的运行方式依旧没有被淘汰那?这是由它的用途决定的;JS 主要用途是用户交互和DOM操作,举例来说假如js同时有两线程,一个线程在某个DOM节点上添加内容,另一个线程却删除了这个节点,这时候浏览器就不知所措了,该以哪个线程为标准那?(为了提高运行性能,新的 html5 里添加了web worker,其能在主线程内添加子线程,但是限制了其无法操作DOM。)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
任务队列(task queue)
由于 JS 是单线程,所以任务的执行就需要排队,一个一个执行,前一个任务结束了,下一个任务才能开始。但是当一个任务是异步任务时,浏览器就需要等待较长时间,才能得到它的返回结果继续执行,中间等待的时间cpu是空闲。JS 的应对方案是,将该任务暂时搁置,去执行其他任务。当有返回结果时再重新回来执行该任务。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
这个暂时搁置,搁置于何处那,答案就是任务队列。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
同步任务是指在主线程上执行的任务,只有前一个任务执行完毕,下一个任务才能执行。 异步任务是指不进入主线程,而是进入任务队列(task queue)的任务,只有主线程任务执行完毕,任务队列的任务才会进入主线程执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
执行栈(JS stack)
首先,我们先来了解一下堆(heap)和栈(stack)的概念。栈是用来静态分配内存的而堆是动态分配内存的,它们都是存在于计算机内存之中。栈是先进后出,堆是先进先出的。js的所有任务都是在js执行栈中执行的。先进入栈的任务后执行,但是大部分时候js执行栈内都只有一个任务。(下文会提及)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
宏任务和微任务(task & Microtask)
上文说道异步任务不在主线程上执行,其实不单单是异步任务,所有的微任务都不在主线程上执行。由此其实我们可以将上文的任务队列称之为微任务队列。宏任务直接在主线程上自行,而微任务需要进入为任务队列,等待执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
我们看一下代码(example1)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
复制代码
这个输出结果是什么那?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
顺序是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
script start
script end
promise1
promise2
setTimeout
复制代码
首先我们视整段代码为一个 script 标签,它作为一个宏任务,直接进入js执行栈中执行:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
输出==script start==;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
遇到setTimeout,而0秒后setTimeout作为一个独立的宏任务加入到"宏任务队列"中。(注意这里说的是宏任务队列,也就是上文所说的主线程);文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
遇到promise,promise完成后的第一个then作为一个独立的微任务加入到“微任务队列”中,第二个then又做为一个微任务加入到微任务的队列中。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
然后输出==script end==;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
现在,我们来理一下:script一整个宏任务执行完毕了,这时候js执行栈是空的,宏任务队列(主线程)中有一个setTimeout,而微任务队列中有两个promise(then)任务。先执行哪个?回想我们之前说的异步任务执行策略,就不难推测,下一个进入js执行栈就是第一个promise(then);文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
输出 ==promise1==;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
然后此时再看宏任务队列和微任务队列。微任务队列还有一个promise(then),所以将这个微任务压入js执行栈执行;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
输出==promise2==;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
此时,微任务队列为空,所以再去执行宏任务队列中的任务,setTimeout;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
输出==setTimeout==;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
总结来说,任务分为宏任务和微任务,对应宏任务队列(主线程)和微任务队列。微任务是在当前正在执行脚本结束之后立即执行的任务。当一个任务执行结束后,JS 执行栈空出来,这时候会首先去微任务队列中寻找任务,当微任务队列不为空时,将一个微任务加入到 JS 执行栈中。当当前的微任务队列为空时,再去执行宏任务队列中的任务。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
如何区分微任务和宏任务:
宏任务(task):是严格按照时间顺序压栈和执行的,所以浏览器能够使得 JavaScript 内部任务与 DOM 任务能够有序的执行。当一个 task 执行结束后,在下一个 task 执行开始前,浏览器可以对页面进行重新渲染。每一个 task 都是需要分配的,例如从用户的点击操作到一个点击事件,渲染HTML文档,同时还有上面例子中的 setTimeout。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
setTimeout 的工作原理相信大家应该都知道,其中的延迟并不是完全精确的,这是因为 setTimeout 它会在延迟时间结束后分配一个新的 task 至 event loop 中,而不是立即执行,所以 setTimeout 的回调函数会等待前面的 task 都执行结束后再运行。这就是为什么 'setTimeout' 会输出在 'script end' 之后,因为 'script end' 是第一个 task 的其中一部分,而 'setTimeout' 则是一个新的 task。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
微任务(Microtask):通常来说就是需要在当前 task 执行结束后立即执行的任务,例如需要对一系列的任务做出回应,或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。microtask 任务队列是一个与 task 任务队列相互独立的队列,microtask 任务将会在每一个 task 任务执行结束之后执行。每一个 task 中产生的 microtask 都将会添加到 microtask 队列中,microtask 中产生的 microtask 将会添加至当前队列的尾部,并且 microtask 会按序的处理完队列中的所有任务。microtask 类型的任务目前包括了 MutationObserver 以及 Promise 的回调函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
每当一个 Promise 被决议(或是被拒绝),便会将其回调函数添加至 microtask 任务队列中作为一个新的 microtask 。这也保证了 Promise 可以异步的执行。所以当我们调用 .then(resolve, reject) 的时候,会立即生成一个新的 microtask 添加至队列中,这就是为什么上面的 'promise1' 和 'promise2' 会输出在 'script end' 之后,因为 microtask 任务队列中的任务必须等待当前 task 执行结束后再执行,而 'promise1' 和 'promise2' 输出在 'setTimeout' 之前,这是因为 'setTimeout' 是一个新的 task,而 microtask 执行在当前 task 结束之后,下一个 task 开始之前。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
进阶版,带你深入task & Microtask(example2):
<body>
<div class="outer">
<div class="inner"></div>
</div>
</body>
<script>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
</script>
复制代码
当我们点击inner这个div的时候会输出什么那?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
顺序是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
click
promise
mutate
click
promise
mutate
timeout
timeout
复制代码
为何是如此那?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
这里要说明的是一个click操作作为一个宏任务,当这个inner的click对应的监听函数执行完后,即视为一个任务的完成,此时执行微任务队列中的promise(then)和 mutationObserver的回调。这两个任务执行完成后微任务队列为空,然后再执行冒泡造成的outter的click。当outter的click任务和微任务都执行完后,才会再去找宏任务队列(主线程)中剩下的两个setTimeout的任务。并将其一个一个的压入执行栈。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
超级进阶版(example3):
当我们在上面的js代码中加入下面这行代码时,会有什么不同吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
inner.click()
复制代码
答案是:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
click
click
promise
mutate
promise
timeout
timeout
复制代码
为何会有如此大的不同那?下面我们来仔细分析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
上一个例子中两个微任务在两个click之间执行,而这个例子中,却是在两个click之后执行的;文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
首先inner.click()触发的事件作为一个任务压入执行栈,由此产生的inner的监听函数函数又做为一个任务压入执行栈,当这个回调函数产生的任务执行完毕后,输出了 click,且微任务队列里面增加promise和mutate,那按上面的说法不是应该执行promise和mutate吗?然而并不是,因为此时 JS 执行栈内的inner.click()还没有执行结束,所以继续inner.click()的事件触发outter的监听函数,由此再输出click,该回调结束后,inner.click()这个任务才算是结束,此时才会去执行微任务队列中的任务。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
简单来说,在这个例子中,由于我们调用 inner.click() ,使得事件监听器的回调函数和当前运行的脚本同步执行而不是异步,所以当前脚本的执行栈会一直压在 JS 执行栈 当中。所以在这个例子中的微任务不会在每一个 click 事件之后执行,而是在两个 click 事件执行完成之后执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
Event Loop
JS 执行栈不断的从主线程中和微任务队列读取任务并执行,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
注:本文所有运行结果皆给予chrome浏览器,其他浏览器或有出入文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
作者:铜板街技术
链接:https://juejin.im/post/5c55aa2851882524c84ef517
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html