JavaScript的运行机制—Event Loop

2019-02-0409:40:27WEB前端开发Comments2,371 views字数 4463阅读模式

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

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/9456.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/9456.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定