Javascript的Event Loop事件循环

220 阅读7分钟

Event Loop

基本概念

Event Loop 是一个很重要的概念,指的是计算机系统的一种运行机制。JavaScript语言就采用这种机制,来解决单线程运行带来的一些问题。

Javascript是一种单线程语言,所有任务都在一个线程上完成。一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。

单线程的JavaScript

概览

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

JavaScript为什么是单线程,难道不能实现为多线程吗?

这跟历史有关系。JavaScript从诞生起就是单线程。原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。后来就约定俗成,JavaScript为一种单线程语言。(Web Worker API可以实现多线程,但是JavaScript本身始终是单线程的。)

如果某个任务很耗时,比如涉及很多I/O(输入/输出)操作,那么线程的运行大概是下面的样子。

上图的绿色部分是程序的运行时间,红色部分是等待时间。可以看到,由于I/O操作很慢,所以这个线程的大部分运行时间都在空等I/O操作的返回结果。这种运行方式称为"同步模式"(synchronous I/O)或"堵塞模式"(blocking I/O)。

简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他线程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。

上图主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行,所以不存在红色的等待时间。等到I/O程序完成操作,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。

可以看到,由于多出了橙色的空闲时间,所以主线程得以运行更多的任务,这就提高了效率。这种运行方式称为"异步模式"(asynchronous I/O)或"非堵塞模式"(non-blocking mode)。

这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果部署得好,JavaScript程序是不会出现堵塞的,这就是为什么node.js平台可以用很少的资源,应付大流量访问的原因。

简单来说,Event Loop就是浏览器为了协调事件处理、脚本执⾏、⽹络请求和渲染等任务⽽制定的⼯作机制。

微任务&宏任务

宏任务

代表⼀个个离散的、独⽴的⼯作单元。浏览器完成⼀个宏任务,在下⼀个宏任务执⾏

开始前,会对⻚⾯进⾏重新渲染。主要包括创建⽂档对象、解析HTML、执⾏主线JS代码以及各种

事件如⻚⾯加载,DOM事件、输⼊、⽹络事件和定时器等。

微任务

微任务是更⼩的任务,是在当前宏任务执⾏结束后⽴即执⾏的任务。如果存在微任务,浏

览器会清空微任务之后再重新渲染。微任务的例⼦有 Promise 回调函数、DOM变化等。

微任务和宏任务的区别

  • 宏任务:DOM渲染后触发,由浏览器规定(Web APIs)
  • 微任务:DOM渲染前执行,微任务是ES6语法规定

思考问题:
DOM渲染,浏览器刷新?

学习

下面的代码输出什么?

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

jakearchibald.com/2015/tasks-…

Event Loop过程

执行过程

简单概括的来说就是下面的过程:

  • Event Loop会不断循环的去取tasks队列的中最老的一个任务推入栈中执行,并在当次循环里依次执行并清空microtask队列里的任务。
  • 执行完microtask队列里的所有的任务,有可能会渲染更新(如果有DOM变更的话)。
  • 执行下一个宏任务的时候,DOM已经重新渲染了。

再精简一点:
在执行下一个宏任务前,要清空微任务队列。

同步代码属于宏任务。微任务队列清空后,浏览器会重新渲染。

DOM渲染

我们知道上面的Event Loop工作机制后,再来看一下DOM渲染。

const main = document.getElementById('main');
const frg = document.createDocumentFragment();

for(let i = 0; i < 10; i++) {
  const li = document.createElement('li');
  li.innerHTML = i;
  frg.appendChild(li);
}
main.appendChild(frg);

new Promise((resolve) => {
  resolve();
}).then(() => {
  console.log('微任务已经执行');
});

setTimeout(() => {
  console.log('宏任务执行');
});

思考

  1. 上面代码在执行完第9行后,页面上会有什么变化?
    ①那我在第10行把main.innerHTML输出出来看一下不就知道了吗!
    【答】:这种方式反应不了页面中的DOM是否已经被渲染,因为我们操作的是DOM对象,DOM对象已经发生变化,当然是可以看到输出的innerHTML,但是真实的DOM并不一定就已经渲染完毕。
    ②或者,我可以在第10行打上断点,然后看一下页面上显示了什么内容不就可以了!

【答】:这种方式也不行。因为在调试模式下,JS对DOM得操作是动态的,也就是说,这这种情况下,操作了DOM,马上就可以在页面上看到DOM的变化,但是在实际执行过程中,JS对DOM的操作并不会立即反应到DOM变化。

  1. 微任务队列清空后,DOM会重新渲染。

那能不能找一种直观的方式验证一下这个说法呢?

当然可以了。

第一种:

const main = document.getElementById('main');
const frg = document.createDocumentFragment();

for(let i = 0; i < 10; i++) {
  const li = document.createElement('li');
  li.innerHTML = i;
  frg.appendChild(li);
}
main.appendChild(frg);

new Promise((resolve) => {
  resolve();
}).then(() => {
  console.log('微任务已经执行');
  alert('dom 还未插入');
});

setTimeout(() => {
  console.log('宏任务执行');
  alert('dom 已经插入');
});

第一种方法,你要了解alert工作机制。
如果我不想用alert,能不能找一种更加直观的方式去验证呢。

第二种:

const main = document.getElementById('main');
const frg = document.createDocumentFragment();

for(let i = 0; i < 10; i++) {
  const li = document.createElement('li');
  li.innerHTML = i;
  frg.appendChild(li);
}
main.appendChild(frg);

new Promise((resolve) => {
  resolve();
}).then(() => {
  console.log('微任务已经执行');
  let sum = 0;
  for(let i=0;i<10000000000;i++){
    sum += i;
  }
});

setTimeout(() => {
  console.log('宏任务执行');
});

输出innerHTML,和打断点观察页面内容,这两种方式都是不行。

知识点检验

async function asycn1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(() => {
  console.log('setTimeout ');
}, 0);

asycn1();

new Promise((resolve) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');