本文将详细介绍javascript中的事件循环event-loopjavascript
javascript是单线程的语言,也就是说,同一个时间只能作一件事。而这个单线程的特性,与它的用途有关,做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?java
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质数组
【排队】浏览器
单线程就意味着,全部任务须要排队,前一个任务结束,才会执行后一个任务。若是前一个任务耗时很长,后一个任务就不得不一直等着网络
var i, t = Date.now()
for (i = 0; i < 100000000; i++) {} console.log(Date.now() - t) // 238
像上面这样,若是排队是由于计算量大,CPU忙不过来,倒也算了数据结构
可是,若是是网络请求就不合适。由于一个网络请求的资源何时返回是不可预知的,这种状况再排队等待就不明智了app
因而,任务分为同步任务和异步任务异步
【同步】函数
若是在函数返回的时候,调用者就可以获得预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的oop
Math.sqrt(2); console.log('Hi');
第一个函数返回时,就拿到了预期的返回值:2的平方根;第二个函数返回时,就看到了预期的效果:在控制台打印了一个字符串
因此这两个函数都是同步的
【异步】
若是在函数返回的时候,调用者还不可以获得预期结果,而是须要在未来经过必定的手段获得,那么这个函数就是异步的
fs.readFile('foo.txt', 'utf8', function(err, data) { console.log(data); });
在上面的代码中,咱们但愿经过fs.readFile
函数读取文件foo.txt中的内容,并打印出来。可是在fs.readFile
函数返回时,咱们指望的结果并不会发生,而是要等到文件所有读取完成以后。若是文件很大的话可能要很长时间
因此,fs.readFile函数是异步的
正是因为JavaScript是单线程的,而异步容易实现非阻塞,因此在JavaScript中对于耗时的操做或者时间不肯定的操做,使用异步就成了必然的选择
从上文能够看出,异步函数
实际上很快就调用完成了。可是后面还有执行异步操做、通知主线程、主线程调用回调函数等不少步骤。咱们把整个过程叫作异步过程
。异步函数的调用在整个异步过程当中,只是一小部分
一个异步过程一般是这样的:主线程发起一个异步请求,异步任务接收请求并告知主线程已收到(异步函数返回);主线程能够继续执行后面的代码,同时异步操做开始执行;执行完成后通知主线程;主线程收到通知后,执行必定的动做(调用回调函数)
所以,一个异步过程包括两个要素:注册函数和回调函数,其中注册函数用来发起异步过程,回调函数用来处理结果
下面的代码中,其中的setTimeout就是异步过程的发起函数,fn是回调函数
setTimeout(fn, 1000);
有一个很重要的问题,如何才算是异步操做执行完成呢?对于不一样类型的异步任务,操做完成的标准不一样
【异步类型】
通常而言,异步任务有如下三种类型
一、普通事件,如click、resize等
二、资源加载,如load、error等
三、定时器,包括setInterval、setTimeout等
下面对这三种类型分别举例说明,下面代码中,鼠标点击div时,就表明任务执行完成了
div.onclick = () => { console.log('click') }
下面代码中,XHR对象的readyState值为4,即已经接收到所有响应数据了,表明任务执行完成
xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ if(xhr.status == 200){ //实际操做 result.innerHTML += xhr.responseText; } } }
下面代码中,过1s后,表明任务执行完成
setTimeout(() => { console.log('timeout') },1000)
对于同步任务来讲,按顺序执行便可;可是,对于异步任务,各任务执行的时间长短不一样,执行完成的时间点也不一样,主线程如何调控异步任务呢?这就用到了消息队列
【消息队列】
有些文章把消息队列称为任务队列,或者叫事件队列,总之是和异步任务相关的队列
能够肯定的是,它是队列这种先入先出的数据结构,和排队是相似的,哪一个异步操做完成的早,就排在前面。不论异步操做什么时候开始执行,只要异步操做执行完成,就能够到消息队列中排队
这样,主线程在空闲的时候,就能够从消息队列中获取消息并执行
消息队列中放的消息具体是什么东西?消息的具体结构固然跟具体的实现有关。可是为了简单起见,能够认为:消息就是注册异步任务时添加的回调函数。
人们把javascript调控同步和异步任务的机制称为事件循环,首先来看事件循环机制的可视化描述
【栈】
函数调用造成了一个栈帧
function foo(b) {
var a = 10; return a + b + 11; } function bar(x) { var y = 3; return foo(x * y); } console.log(bar(7));
当调用bar
时,建立了第一个帧 ,帧中包含了bar
的参数和局部变量。当bar
调用foo
时,第二个帧就被建立,并被压到第一个帧之上,帧中包含了foo
的参数和局部变量。当foo
返回时,最上层的帧就被弹出栈(剩下bar
函数的调用帧 )。当bar
返回的时候,栈就空了
【堆】
对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域
【队列】
一个 JavaScript 运行时包含了一个待处理的消息队列。每个消息都与一个函数相关联。当栈拥有足够内存时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及于是建立了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束
下面来详细介绍事件循环。下图中,主线程运行的时候,产生堆和栈,栈中的代码调用各类外部API,异步操做执行完成后,就在消息队列中排队。只要栈中的代码执行完毕,主线程就会去读取消息队列,依次执行那些异步任务所对应的回调函数
详细步骤以下:
一、全部同步任务都在主线程上执行,造成一个执行栈
二、主线程以外,还存在一个"消息队列"。只要异步操做执行完成,就到消息队列中排队
三、一旦执行栈中的全部同步任务执行完毕,系统就会按次序读取消息队列中的异步任务,因而被读取的异步任务结束等待状态,进入执行栈,开始执行
四、主线程不断重复上面的第三步
【循环】
从代码执行顺序的角度来看,程序最开始是按代码顺序执行代码的,遇到同步任务,马上执行;遇到异步任务,则只是调用异步函数发起异步请求。此时,异步任务开始执行异步操做,执行完成后到消息队列中排队。程序按照代码顺序执行完毕后,查询消息队列中是否有等待的消息。若是有,则按照次序从消息队列中把消息放到执行栈中执行。执行完毕后,再从消息队列中获取消息,再执行,不断重复。
因为主线程不断的重复得到消息、执行消息、再取消息、再执行。因此,这种机制被称为事件循环
用代码表示大概是这样:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
若是当前没有任何消息queue.waitForMessage
会等待同步消息到达
【事件】
为何叫事件循环?而不叫任务循环或消息循环。究其缘由是消息队列中的每条消息实际上都对应着一个事件
DOM操做对应的是DOM事件,资源加载操做对应的是加载事件,而定时器操做能够看作对应一个“时间到了”的事件
下面以一个实例来解释事件循环机制
console.log(1) div.onclick = () => {console.log('click')} console.log(2) setTimeout(() => {console.log('timeout')},1000)
一、执行第一行代码,第一行是一个同步任务,控制台显示1
二、执行第二行代码,第二行是一个异步任务,发起异步请求,能够在任意时刻执行鼠标点击的异步操做
三、执行第三行代码,第三行是一个同步任务,控制台显示2
四、执行第四行代码,第四行是一个异步任务,发起异步请求,1s后执行定时器任务
五、假设从执行第四行代码的1s内,执行了鼠标点击,则鼠标任务在消息队列中排到首位
六、从执行第四行代码1s后,定时器任务到消息队列中排到第二位
七、如今同步任务已经执行完毕,则从消息队列中按照次序把异步任务放到执行栈中执行
八、则控制台依次显示'click‘、'timeout'
九、过了一段时间后,又执行了一次鼠标点击,因为消息队列中已经空了,则鼠标任务在消息队列中排到首位
十、同步任务执行完毕后,再从消息队列中按照次序把异步任务放到执行栈中执行
十一、 则控制台显示'click'
【异步过程】
下面以一个实例来解释一次完整的异步过程
div.onclick = function fn(){console.log('click')}
一、主线程经过调用异步函数div.onclick发起异步请求
二、在某一时刻,执行异步操做,即鼠标点击
三、接着,回调函数fn到消息队列中排队
四、主线程从消息队列中读取fn到执行栈中
五、而后在执行栈中执行fn里面的代码console.log('click')
六、因而,控制台显示'click'
每个消息完整的执行后,其它消息才会被执行。这点提供了一些优秀的特性,包括每当一个函数运行时,它就不能被抢占,而且在其余代码运行以前彻底运行
这个模型的一个缺点在于当一个消息须要太长时间才能完成,Web应用没法处理用户的交互,例如点击或滚动
因而,对于这种状况的常见优化是同步变异步
一个例子是建立WebQQ的QQ好友列表。列表中一般会有成百上千个好友,若是一个好友用一个节点来表示,在页面中渲染这个列表的时候,可能要一次性往页面中建立成百上千个节点
在短期内往页面中大量添加DOM节点显然也会让浏览器吃不消,看到的结果每每就是浏览器的卡顿甚至假死。代码以下:
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); // 假设 ary 装载了 1000 个好友的数据 }; var renderFriendList = function( data ){ for ( var i = 0, l = data.length; i < l; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = i; document.body.appendChild( div ); } }; renderFriendList( ary );
这个问题的解决方案之一是数组分块技术,下面的timeChunk函数让建立节点的工做分批进行,好比把1秒钟建立1000个节点,改成每隔200毫秒建立8个节点
function chunk(array,process,context){ setTimeout(function(){ //取出下一个条目并处理 var item = array.shift(); process.call(context,item); //若还有条目,再设置另外一个定时器 if(array.length > 0){ setTimeout(arguments.callee,100); } },100); }
var data = [1,2,3,4,5,6,7,8,9,0]; function printValue(item){ var div = document.getElementById('myDiv'); div.innerHTML += item + '<br>'; } chunk(data.concat(),printValue);
数组分块的重要性在于它能够将多个项目的处理在消息队列上分开,在每一个项目处理以后,给予其余的异步任务的执行机会,这样就可能避免长时间运行脚本的错误。一旦某个函数须要花50ms以上的时间完成,那么最好看看可否将任务分割为一系列可使用定时器的小任务