带你了解JavaScript的运行机制——Event Loop

1.js是单线程的。

首先,众所周知,js是单线程的,为什么这种低效的运行方式依旧没有被淘汰那?这是由它的用途决定的;js主要用途是用户交互和DOM操作,举例来说假如js同时有两线程,一个线程在某个DOM节点上添加内容,另一个线程却删除了这个节点,这时候浏览器就不知所措了,该以哪个线程为标准那?(为了提高运行性能,新的html5里添加了web worker,其能在主线程内添加子线程,但是限制了其无法操作DOM。)

2.任务队列(task queue)

由于js是单线程,所以任务的执行就需要排队,一个一个执行,前一个任务结束了,下一个任务才能开始。但是当一个任务是异步任务时,浏览器就需要等待较长时间,才能得到它的返回结果继续执行,中间等待的时间cpu是空闲。js的应对方案是,将该任务暂时搁置,去执行其他任务。当有返回结果时再重新回来执行该任务。

这个暂时搁置,搁置于何处那,答案就是任务队列

同步任务是指在主线程上执行的任务,只有前一个任务执行完毕,下一个任务才能执行。
异步任务是指不进入主线程,而是进入任务队列(task queue)的任务,只有主线程任务执行完毕,任务队列的任务才会进入主线程执行。

3.执行栈(JS stack)

首先,我们先来了解一下堆(heap)和栈(stack)的概念。栈是用来静态分配内存的而堆是动态分配内存的,它们都是存在于计算机内存之中。栈是先进后出,堆是先进先出的。js的所有任务都是在js执行栈中执行的。先进入栈的任务后执行,但是大部分时候js执行栈内都只有一个任务。(下文会提及)

4.宏任务和微任务(task & Microtask)

上文说道异步任务不在主线程上执行,其实不单单是异步任务,所有的微任务都不在主线程上执行。由此其实我们可以将上文的任务队列称之为微任务队列。宏任务直接在主线程上自行,而微任务需要进入为任务队列,等待执行。

我们看一下代码(example1)

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');

这个输出结果是什么那?

顺序是:

script start
script end
promise1
promise2
setTimeout

首先我们视整段代码为一个script标签,它作为一个宏任务,直接进入js执行栈中执行:

输出script start

遇到setTimeout,而0秒后setTimeout作为一个独立的宏任务加入到"宏任务队列"中。(注意这里说的是宏任务队列,也就是上文所说的主线程);

遇到promise,promise完成后的第一个then作为一个独立的微任务加入到“微任务队列”中,第二个then又做为一个微任务加入到微任务的队列中。

然后输出script end

现在,我们来理一下:script一整个宏任务执行完毕了,这时候js执行栈是空的,宏任务队列(主线程)中有一个setTimeout,而微任务队列中有两个promise(then)任务。先执行哪个?回想我们之前说的异步任务执行策略,就不难推测,下一个进入js执行栈就是第一个promise(then);

输出 promise1

然后此时再看宏任务队列和微任务队列。微任务队列还有一个promise(then),所以将这个微任务压入js执行栈执行;

输出promise2

此时,微任务队列为空,所以再去执行宏任务队列中的任务,setTimeout;

输出setTimeout

总结来说,任务分为宏任务和微任务,对应宏任务队列(主线程)和微任务队列。微任务是在当前正在执行脚本结束之后立即执行的任务。当一个任务执行结束后,js执行栈空出来,这时候会首先去微任务队列中寻找任务,当微任务队列不为空时,将一个微任务加入到js执行栈中。当当前的微任务队列为空时,再去执行宏任务队列中的任务。

如何区分微任务和宏任务:

宏任务(task):是严格按照时间顺序压栈和执行的,所以浏览器能够使得 JavaScript 内部任务与 DOM 任务能够有序的执行。当一个 task 执行结束后,在下一个 task 执行开始前,浏览器可以对页面进行重新渲染。每一个 task 都是需要分配的,例如从用户的点击操作到一个点击事件,渲染HTML文档,同时还有上面例子中的 setTimeout。

setTimeout 的工作原理相信大家应该都知道,其中的延迟并不是完全精确的,这是因为 setTimeout 它会在延迟时间结束后分配一个新的 task 至 event loop 中,而不是立即执行,所以 setTimeout 的回调函数会等待前面的 task 都执行结束后再运行。这就是为什么 ‘setTimeout’ 会输出在 ‘script end’ 之后,因为 ‘script end’ 是第一个 task 的其中一部分,而 ‘setTimeout’ 则是一个新的 task。

微任务(Microtask):通常来说就是需要在当前 task 执行结束后立即执行的任务,例如需要对一系列的任务做出回应,或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。microtask 任务队列是一个与 task 任务队列相互独立的队列,microtask 任务将会在每一个 task 任务执行结束之后执行。每一个 task 中产生的 microtask 都将会添加到 microtask 队列中,microtask 中产生的 microtask 将会添加至当前队列的尾部,并且 microtask 会按序的处理完队列中的所有任务。microtask 类型的任务目前包括了 MutationObserver 以及 Promise 的回调函数。

每当一个 Promise 被决议(或是被拒绝),便会将其回调函数添加至 microtask 任务队列中作为一个新的 microtask 。这也保证了 Promise 可以异步的执行。所以当我们调用 .then(resolve, reject) 的时候,会立即生成一个新的 microtask 添加至队列中,这就是为什么上面的 ‘promise1’ 和 ‘promise2’ 会输出在 ‘script end’ 之后,因为 microtask 任务队列中的任务必须等待当前 task 执行结束后再执行,而 ‘promise1’ 和 ‘promise2’ 输出在 ‘setTimeout’ 之前,这是因为 ‘setTimeout’ 是一个新的 task,而 microtask 执行在当前 task 结束之后,下一个 task 开始之前。


进阶版,带你深入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的时候会输出什么那?

顺序是:

click
promise
mutate
click
promise
mutate
timeout
timeout

为何是如此那?

这里要说明的是一个click操作作为一个宏任务,当这个inner的click对应的监听函数执行完后,即视为一个任务的完成,此时执行微任务队列中的promise(then)和 mutationObserver的回调。这两个任务执行完成后微任务队列为空,然后再执行冒泡造成的outter的click。当outter的click任务和微任务都执行完后,才会再去找宏任务队列(主线程)中剩下的两个setTimeout的任务。并将其一个一个的压入执行栈。


超级进阶版(example3):

当我们在上面的js代码中加入下面这行代码时,会有什么不同吗?

inner.click()

答案是:

click
click
promise
mutate
promise
timeout
timeout

为何会有如此大的不同那?下面我们来仔细分析:

上一个例子中两个微任务在两个click之间执行,而这个例子中,却是在两个click之后执行的;

首先inner.click()触发的事件作为一个任务压入执行栈,由此产生的inner的监听函数函数又做为一个任务压入执行栈,当这个回调函数产生的任务执行完毕后,输出了 click,且微任务队列里面增加promise和mutate,那按上面的说法不是应该执行promise和mutate吗?然而并不是,因为此时js执行栈内的inner.click()还没有执行结束,所以继续inner.click()的事件触发outter的监听函数,由此再输出click,该回调结束后,inner.click()这个任务才算是结束,此时才会去执行微任务队列中的任务。

简单来说,在这个例子中,由于我们调用 inner.click() ,使得事件监听器的回调函数和当前运行的脚本同步执行而不是异步,所以当前脚本的执行栈会一直压在 JS 执行栈 当中。所以在这个例子中的微任务不会在每一个 click 事件之后执行,而是在两个 click 事件执行完成之后执行。

5.Event Loop

js执行栈不断的从主线程中和微任务队列读取任务并执行,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

:本文所有运行结果皆给予chrome浏览器,其他浏览器或有出入

参考文章https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

猜你喜欢

转载自blog.csdn.net/qq_33619285/article/details/84525669