最开始见到event loop这个字眼是在一次笔试上,请简要描述你对event loop的理解。作为前端最底层的小白,当时看到event loop都不知道是什么意思。回来之后赶紧科普一下,event loop翻译成中文是事件循环。题目大概考察的是对js中事件机制的理解。翻阅众多博客,有长话短说的也有详细讲解的。以下是自己看完博客后的理解。
一、js是单线程的
js语言的一大特点就是单线程,也就是说js在同一时间只能做一件事,那么有人会问,为什么不是多线程。这样js代码执行起来岂不是更快。
首先,js的这个特性与它的用途有关。js作为浏览器脚本语言,js的主要功能是与用户进行交互,以及操作dom。如果js是多线程,比如有两个线程,一个线程是在dom上添加了一段文字,另一个线程在dom上减少了一段文字,两个线程同时执行,那么浏览器不知道以哪个线程为准,执行就会出现问题。为了避免这些问题的发生,js从诞生起就一直是单线程,这已经成了js的一大特点,将来也不会改变。
为了利用多核cpu的计算能力,H5新增了Web Worker标准,允许js创建多个线程,但是子线程完全受主线程的控制,且不可以操作dom,所以在本质上并没有改变js单线程的特性。
二、任务队列(Task Queue)
单线程意味着所有的任务都需要排队依次执行,前一个任务结束后才开始进行下一个任务。如果前一个任务执行的时间非常长,那么后一个任务也得必须等待。
如果是因为运算量过大导致cpu忙不过来倒是可以理解,但是更多时候cpu处于闲置状态,因为IO设备(输入输出设备)很慢(比如ajax从服务器获取数据),必须等待结果出来以后才往下继续进行。
js的设计者意识到这样的问题,主线程可以完全不管IO设备,挂起处于这样状态的任务,先执行排在后边的任务。等到IO设备返回了结果,再回过头去执行挂起的任务。
于是,js中任务分为两种,同步任务(synchronous)和异步任务(asychronous)。同步任务指的是在主线程上排队执行的任务,只要前一个执行完了,后一个才去执行。异步任务指的是不进入主线程、而进入“任务队列(task queue)”的任务,只有“任务队列”通知主线程,某个任务队列可以执行了,该任务才会进入主线程去执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步的任务异步执行)
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
- 除主线程之外,还存在一个“任务队列(task queue)”,只有异步任务有了运行结果,就会在“任务队列”中放置一个事件。
- 一旦执行栈中所有同步任务执行完毕,系统就会读取“任务队列”,看看里边有哪些事件。那些对应的异步任务,才会结束等待状态,进入执行栈,开始执行。
- 主线程不断重复执行第三步
只要主线程空了,就回去读取“任务队列”,这就是js的运行机制。这个过程会不断重复。
三、事件和回调函数
“任务队列”是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就会在“任务队列”中添加一个事件,表示相关的异步任务可以进入“执行栈”了。主线程读取“任务队列”,就是在读取里边有哪些事件。
“任务队列”中的事件,除了IO设备的事件以外,还包括用户产生的事件(鼠标点击,页面滚动等)。只要指定过回调函数,这些事件发生时就会进入“任务队列”,等待主线程的读取。
四、event loop
主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为event loop(事件循环)
主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在“任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会读取“任务队列”,依次执行那些事件所对应的回调函数。
执行栈中的代码(同步任务),总是在读取“任务队列”(异步任务)之前执行。例如
var req = new XMLHttpRequest(); req.open("get",url); req.onload = function(){}; req.onerror = function(){}; req.send();
上述代码中,req.send()是ajax向服务器发送数据,它是一个异步任务,意味着只有当前脚本全部执行完毕后,系统才回去读取“任务队列”。所以下边是等价写法
var req = new XMLHttpRequest(); req.open("get",url); req.send(); req.onload = function(){}; req.onerror = function(){};
上边的意思就是说,指定函数回调的部分(onload和onerror),在send方法的前边或后边无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们才去执行“任务队列”。
五、定时器
除了放置异步任务的事件,“任务队列”还可以放置定时事件,即指定某些代码在多少时间后执行。这叫做定时器功能,也就是定时执行的代码。定时器功能主要由setTimeOut和SetInterval这两个函数来完成。它们的内部运行机制完全一样,它们两个区别在于前者执行一次,后者可以反复执行。
console.log(1); setTimeout(function(){console.log(2)},1); console.log(3); //打印结果 //1,3,2
因为console.log(2)是延迟1毫秒执行的。但是,如果把1写成0,
console.log(1); setTimeout(function(){console.log(2)},0); console.log(3);
执行结果依然是1,3,2
第二个参数设置为0,表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。
说明:setTimeout(fn,0)含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说尽可能早的执行。它在“任务队列”的尾部添加一个事件,因此要等到同步任务和“任务队列”现有的事件都处理完,才会得到执行。
H5标准说明了,setTimeout第二个参数的最小值(最短间隔)不得低于4毫秒。如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。
setTimeout只是将事件插入到“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它的指定的回调函数。要是当前代码执行时间太长,有可能要等很久,所以没办法保证,一定会在setTimeout指定时间执行。
六、node.js中的event loop
暂无