Javascript异步编程 Eventloop运行机制

为什么JAVASCRIPT是单线程

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

同步任务和异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。(在异步操作完成后所要做的任务,它们通常以回调函数或者 Promise 的形式被放入事件队列)

异步操作和异步任务

  • 异步操作一般包括 网络请求 、文件读取 、数据库处理
  • 异步任务一般包括 setTimout / setInterval 、Promise 、requestAnimationFrame ( 浏览器独有 ) 、setImmediate ( Node 独有 ) 、process.nextTick ( Node 独有 )
  • 在浏览器端与在 Node 端的 Event Loop 机制是有所不同的

事件和回调函数

  • 任务队列是一个事件的队列(也可以理解成消息的队列)”任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。
  • 所谓”回调函数”(callback, 指在另一个函数执行完成之后被调用的函数),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
  • 回调和事件处理程序本质上并无区别,只是在不同情况下,不同的叫法。

事件循环(Event Loop)

JavaScript引擎负责解析,执行JavaScript代码,但它并不能单独运行,通常都得有一个宿主环境,一般如浏览器或Node服务器,这些宿主环境创建主线程,提供一种机制,调用JavaScript引擎完成多个JavaScript代码块的调度,执行(是的,JavaScript代码都是按块执行的),并且循环不断,这种机制就称为事件循环(Event Loop)。

事件循环的流程分解如下:

  1. 宿主环境为JavaScript创建线程时(主线程运行),会创建堆(heap)和栈(stack),堆内存储JavaScript对象,栈内存储执行上下文;
  2. 栈内执行上下文的同步任务按序执行,执行完即退栈,而当异步任务执行时,该异步任务进入等待状态(不入栈),同时通知线程:当触发该事件时(或该异步操作响应返回时),需向任务队列插入一个事件消息;
  3. 当事件触发或响应返回时,线程向任务队列插入该事件消息(包含事件及回调);
  4. 当栈内同步任务执行完毕后,线程从任务队列取出一个事件消息,其对应异步任务(函数)入栈,执行回调函数,如果未绑定回调,这个消息会被丢弃,执行完任务后退栈;
  5. 当线程空闲(即执行栈清空)时继续拉取任务队列下一轮消息(next tick,事件循环流转一次称为一次tick)。
1
2
3
4
5
6
7
8
9
10
11
var ele = document.querySelector('body');
function clickCb(event) {
console.log('clicked');
}
function bindEvent(callback) {
ele.addEventListener('click', callback);
}
bindEvent(clickCb);

image

setTimeout()

  • 除了放置异步任务的事件,”任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做”定时器”(timer)功能,也就是定时执行的代码。定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。
  • 注意:setTimeout()只是将事件插入了”任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
1
2
3
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3); //1,3,2
1
2
setTimeout(function(){console.log(1);}, 0);
console.log(2); //2,1
  • setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在”任务队列”的尾部添加一个事件,因此要等到同步任务和”任务队列”现有的事件都处理完,才会得到执行。