JavaScript 事件循环

225 阅读5分钟

JavaScript 事件循环

JS是单线程的脚本语言,在同一时间,只能做同一件事,为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程阻塞,Event Loop 方案应运而生.

单线程意味着,JavaScript 代码在执行的任何时候,都只有一个主线程来处理所有的任务。

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如 I/O 事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

浏览器环境下 js 引擎的事件循环机制

执行栈

当执行某个函数、用户点击一次鼠标,Ajax 完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入任务队列中,等待主线程读取,遵循先进先出原则。

执行任务队列中的某个任务,这个被执行的任务就称为执行栈。

主线程

要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。 主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。 当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列Task Queue

要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。

  • 主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。

当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。 当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。 不太理解的话,可以运行一下下面的代码,或者点击一下这个 demo 结果是当 a、b、c 函数都执行完成之后,三个 setTimeout 才会依次执行。

let a = () => {
  setTimeout(() => {
    console.log("任务队列函数1");
  }, 0);
  for (let i = 0; i < 5000; i++) {
    console.log("a的for循环");
  }
  console.log("a事件执行完");
};
let b = () => {
  setTimeout(() => {
    console.log("任务队列函数2");
  }, 0);
  for (let i = 0; i < 5000; i++) {
    console.log("b的for循环");
  }
  console.log("b事件执行完");
};
let c = () => {
  setTimeout(() => {
    console.log("任务队列函数3");
  }, 0);
  for (let i = 0; i < 5000; i++) {
    console.log("c的for循环");
  }
  console.log("c事件执行完");
};
a();
b();
c();
// 当a、b、c函数都执行完成之后,三个setTimeout才会依次执行

异步执行的运行机制

  • 所有任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。

主线程不断重复上面的第三步

JS 引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js 会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环

微任务与宏任务

不同的异步任务被分为两类:微任务(micro task)宏任务(macro task)

微任务(micro task) 优先于 task 执行,所以如果有需要优先执行的逻辑,放入 微任务(micro task) 队列会比 task 更早的被执行。

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

promise 异步是指的 then()方法,而不是指它的构造函数执行;

Promise 构造函数是同步执行的,resolve()/reject()后的代码也会执行。Promise.then()中的函数是异步执行的

  • 宏任务(macro task) 一个 event loop 有一个或者多个 task 队列。task 任务源非常宽泛,比如 ajaxonload,click 事件,基本上我们经常绑定的各种事件都是 task 任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate 也是 task 任务源。总结来说 task 任务源:

    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • UI rendering
  • 微任务(micro task)

    • new Promise()
    • new MutaionObserver()
setTimeout(function() {
  console.log(1);
});

new Promise(function(resolve, reject) {
  console.log(2);
  resolve(3);
}).then(function(val) {
  console.log(val);
});

//2 3 1