⤴Top⤴

事件循环

博客分类: 前端

事件循环

事件循环

进程和线程

进程(process) 是系统进行资源分配和调度的基本单位,任一时刻,单核CPU总是运行一个进程,其他进程处于非运行状态。

线程(thread) 是系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

一个进程可以包括多个线程,一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。而通过互斥锁(Mutex),可防止多个线程同时读写某一块内存区域。信号量(Semaphore) 适用于控制一个仅支持有限个用户的共享资源,是一种不需要使用忙碌等待(busy waiting)的方法。

调用栈(Call Stack)

每次调用一个函数,都要为该次调用的函数实例分配栈空间,即栈帧(Stack Frame)调用栈(执行栈)就是正在使用的栈空间,由多个嵌套调用函数所使用的栈帧组成,实行先进后出(FILO)。

function foo(b) {
  var a = 1;
  return a + b + 2;
}

function bar(x) {
  var y = 3;
  return foo(x * y) + 1;
}

bar(520); // 1564
  1. 当调用 bar 时,创建了第一个帧,帧中包含了 bar 的参数和局部变量;
  2. 当 bar 调用 foo 时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了 foo 的参数和局部变量;
  3. 当 foo 返回时,最上层的帧就被弹出栈(剩下 bar 函数的调用帧);
  4. 当 bar 返回的时候,栈被清空。

事件循环(Event Loop)

Event Loop

JavaScript 属于单线程语言,执行的任务可分为同步和异步,ES6 诞生以前,异步编程的方法,大概有下列四种:

在主线程中,如果有定时器或者其他异步操作,他们会被添加到浏览器 Event Table 事件表(Web APIS)中,当事件(timeout、click、mouse move)满足触发条件后,它会将其发送至 事件队列(Event Queue),实行先进先出。

事件循环是个进程,会持续监测调用栈是否为空(只剩下栈底的全局上下文),若为空,则监测事件队列,将里面的事件移至调用栈执行,如此循环。

Javascript Event Loop Visual Representation

事件循环在线测试地址可以戳这里 👈👈

定时器

调用 setTimeout 函数会在一个时间段后在队列中添加一个事件。这个时间段作为函数的第二个参数被传入。如果队列中没有其它事件,事件会被马上处理。但是,如果有其它事件,setTimeout 事件必须等待其它事件处理完。因此第二个参数仅仅表示最少的时间 而非确切的时间。同样在零延迟调用 setTimeout 时,其并不是过了给定的时间间隔后就马上执行回调函数,其等待的时间基于队列里正在等待的事件数量。

console.log('start');

setTimeout(function(){
  console.log('hello');
}, 200);

setTimeout(function(){
  console.log('world');
}, 300);

// 模拟阻塞
for (var i = 0; i <= 10000; i++){
  console.log(i);
}

setTimeout(function(){
  console.log('Tate');
}, 100);

console.log('end');
// start
// 1...10000
// end
// hello
// world
// Tate

微任务 / 宏任务

任务源(task resource) 分为两种,不同的任务会放进不同的任务队列之中:

我们先看下宏任务和微任务执行的大致情况,看下面栗子 🌰:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
});

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

// script start
// script end
// promise1
// promise2
// setTimeout

让我们来看一个更复杂的栗子 🌰:

outer
inner
<script>
  // Let's get hold of those elements
  var outer = document.querySelector('.outer');
  var inner = document.querySelector('.inner');

  // Let's listen for attribute changes on the
  // outer element
  new MutationObserver(function() {
    console.log('mutate');
  }).observe(outer, {
    attributes: true
  });

  // Here's a click listener…
  function onClick() {
    console.log('click'); // 直接执行

    setTimeout(function() { // 注册宏任务
      console.log('timeout');
    }, 0);

    Promise.resolve().then(function() { // 注册微任务
      console.log('promise');
    });

    outer.setAttribute('data-random', Math.random()); // DOM 属性修改。触发微任务
  }

  // …which we'll attach to both elements
  inner.addEventListener('click', onClick);
  outer.addEventListener('click', onClick);
  // inner.click()
</script>
<script>
  for (let i = 0; i <= 1e+9; i++) {
    if (i === 1e+9) {
      // 大概需要执行3秒
      console.log('script3')
    }
  }
  console.log('script2')
</script>

点击 inner 后,我们看现代浏览器打印的顺序:

click
promise
mutate
click
promise
mutate
timeout * 2
  1. 我们可以看到,当我们点击时,创建了一个宏任务,此时执行同步代码,打印 “click”。同时 Mutation observer and promise callbacks are queued as microtasks. The setTimeout callback is queued as a task
  2. 同步代码执行完后,虽然此时我们正处于 mid-task(因为还有冒泡),但调用栈为空,此时会检测是否存在微任务,有则执行,打印 “promise” 和 “mutate”
  3. 由于 click 冒泡,会触发第二次 click 事件。此过程同上
  4. 在执行完同步代码和微任务后,会再次检测是否存在宏任务并执行,打印两次 “timeout”

总结一下:

那么当我们手动去执行 inner.click() 会发生什么呢,我们看看打印顺序:

click * 2
promise
mutate
promise
script3
script2
timeout * 2
  1. 同上述步骤 1,打印 “click”
  2. 此刻调用栈并不为空,因此无法执行队列里的微任务,继续执行该宏任务,重复步骤 1,打印 “click”
  3. 该宏任务结束后,开始执行队列里的微任务,先进先出,依次打印 “promise” –> “mutate” –> “promise”
  4. 执行完微任务后会再次检测是否存在宏任务,打印 “timeout” * 2

那么问题来了,为啥步骤 2 调用栈不为空呢,是因为此时 click 会导致事件分发(dispatch event),所以在监听器回调之间 JS 执行栈不为空,而上述的这个规则保证了微任务不会打断正在执行的 js,这意味着我们不能在监听器回调之间执行微任务,微任务会在监听器之后执行。

而这里 “mutate” 只打印一次的原因是 MutationObserver 的监听不是同时触发多次,而是多次修改只会有一次回调被触发:

// 只会输出一次 ovserver
new MutationObserver(_ => {
  console.log('observer')
}).observe(document.body, {
  attributes: true
})

document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())

参考链接

  1. MDN - 并发模型与事件循环
  2. 干货 原来你是这样的 setTimeout by iKcamp
  3. Understanding JS: The Event Loop By Alexander Kondov
  4. 栈帧 Stack Frame By Eleveneat
  5. Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more — Part 1 By Gaurav Pandvia
  6. Understanding the JavaScript call stack By Charles Freeborn Eteure
  7. Tasks, microtasks, queues and schedules By Jake
  8. 事件循环在线演示