javascript中的EventLoop
本文介绍 javascript 引擎中的事件循环(EventLoop)是如何工作的。
数据在内存中的存储方式
在 javascript 中,数据类型分为 基本类型
和 引用类型
。那么 基础类型
是存储在 栈内存(stack)
中的简单数据,按值存取,例如:Boolean,String,Number,undefined,null,Symbol,BigInt。
而 引用类型
(Object,Function)则是存储在 堆内存(heap)
中,并在 栈内存
中存储着该 引用类型
数据的一个引用,指向该数据在 堆内存
的存储地址。
1 | var a = '' |
上面代码中各变量在内存中的存储形式如下:
执行栈(call stack)
javascript 中,变量声明,函数调用都会按照程序的逻辑依次被压入栈内执行。
观察下面的代码:
1 | // main.js |
我们在主程序 main.js
中依次定义了 multiply
, calcSquare
和 printSquare
这3个函数。在 printSquare
中调用了 calcSquare
,而 calcSquare
中又调用了 multiply
。
主程序开始后,printSquare()
首先被压入执行栈,随后 calcSquare()
压入执行栈,最后是 multiply()
。
栈具有 先进后出,后进先出
的特点,所以位于栈顶的函数先执行,依次类推。
以下是进栈后的情况:
现在,让我们看看上面代码在执行栈内执行的动态示意图:
栈内异常
如果代码在栈内执行遇到错误,javascript 引擎会抛出如下错误,并终止主程序的继续执行。
栈满错误
有时,由于程序过于庞大或代码编写失误,导致栈内任务剧增,使得执行栈满,javascript 引擎会抛出如下错误。
同步阻塞
在主线程执行栈中,只能存在同步代码。也就是说,在执行栈中的任务都是同步任务。如果存在耗时较长的同步任务,那么我们的程序就会阻塞,直到上一个任务完成才会继续向下执行。
例如下面这样的代码:
1 | var a = $.getSynchronous("/a"); |
在上方代码中,有3行耗时较长的同步代码,在这 3 行代码执行完之前,后面的打印操作便会一直处在等待状态(阻塞),那么在这个阻塞的事件段内,用户界面一直会处于白屏或 loading 态,这对于用户体验是很糟糕的。
异步回调
观察下面的代码,思考其打印结果是什么?
1 | console.log("Hello"); |
打印结果:
1 | Hello |
那么,为什么会是这样的结果呢?它的执行过程是怎么样的?
让我们再看一下上面代码的执行过程:
由于执行栈中只能存在同步代码,从执行栈内的动态示意图来看,同步的 console.log('Hello')
, setTimeout(callback)
(callback并没有被立即执行), console.log('Hi')
被一次压入了执行栈内,而 setTimeout
中的回调函数最后被压入执行栈中。
事件循环
到这里,终于可以谈一谈 事件循环
了,当我们的代码中出现了异步任务时,javascript 引擎是如何工作的呢?
首先,javascript 引擎在完成代码解析后,将同步代码依次压入执行栈中,在遇到异步代码,例如:DOM 事件,AJAX请求,setTimeout/setInterval 这样的异步任务时,会在异步任务完成后,将其回调函数推进一个叫做 回调队列(Callback Queue)
的队列中,等待执行栈内的任务结束后,再依次从 回调队列
的队头取出回调函数压入执行栈中继续执行。
再让我们站在 事件循环
的角度看一次上面代码的执行过程:
如果 setTimeout(callback, 0) 会是怎样?
1 | console.log("Hi"); |
我们直接看上面代码的执行效果:
没错!依然是同步的任务先执行,异步任务的回调函数依然会在异步时间到的时候被推进 回调队列
中,在同步任务结束后执行栈空时,回调队列
中的任务才会被压进 执行栈
。
在这里的延时时间 0
,并不能阻碍 setTimeout()
是一个异步代码,其回调函数依然会被推进 回调队列
中。