下面的内容解释了一个理论上的模型。现代 JavaScript 引擎着重实现和优化了描述的几个语义。html
函数调用造成了一个栈帧。前端
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
返回的时候,栈就空了。node
对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域。git
一个 JavaScript 运行时包含了一个待处理的消息队列。每个消息都有一个为了处理这个消息相关联的函数。github
在事件循环时,runtime (运行时)老是从最早进入队列的一个消息开始处理队列中的消息。正因如此,这个消息就会被移出队列,并将其做为输入参数调用与之关联的函数。为了使用这个函数,调用一个函数老是会为其创造一个新的栈帧( stack frame),一如既往。ajax
函数的处理会一直进行直到执行栈再次为空;而后事件循环(event loop)将会处理队列中的下一个消息(若是还有的话)。编程
之因此称为事件循环,是由于它常常被用于相似以下的方式来实现:api
while (queue.waitForMessage()) { queue.processNextMessage(); }
若是当前没有任何消息queue.waitForMessage
会等待同步消息到达。浏览器
JavaScript语言的一大特色就是单线程,也就是说,同一个时间只能作一件事。那么,为何JavaScript不能有多个线程呢?这样能提升效率啊。网络
JavaScript的单线程,与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?
因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。
说到js的单线程(single threaded)和异步(asynchronous),不少同窗不由会想,这不是自相矛盾么?其实,单线程和异步确实不能同时成为一个语言的特性。js选择了成为单线程的语言,因此它自己不多是异步的,但js的宿主环境(好比浏览器,Node)是多线程的,宿主环境经过某种方式(事件驱动,下文会讲)使得js具有了异步的属性。往下看,你会发现js的机制是多么的简单高效!
js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务造成一个任务队列排队等候执行,但前端的某些任务是很是耗时的,好比网络请求,定时器和事件监听,若是让他们和别的任务同样,都老老实实的排队等待执行的话,执行效率会很是的低,甚至致使页面的假死。因此,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的。下图说明了浏览器的主要线程。
图片来自popAnt 画得太好,忍不住引过来 (http://blog.csdn.net/kfanning/article/details/5768776)
刚才说到浏览器为网络请求这样的异步任务单独开了一个线程,那么问题来了,这些异步任务完成后,主线程怎么知道呢?答案就是回调函数,整个程序是事件驱动的,每一个事件都会绑定相应的回调函数,举个栗子,有段代码设置了一个定时器
setTimeout(function(){ console.log(time is out); },50);
执行这段代码的时候,浏览器异步执行计时操做,当50ms到了后,会触发定时事件,这个时候,就会把回调函数放到任务队列里。整个程序就是经过这样的一个个事件驱动起来的。
因此说,js是一直是单线程的,浏览器才是实现异步的那个家伙。
js一直在作一个工做,就是从任务队列里提取任务,放到主线程里执行。下面咱们来进行更深一步的理解。
图片来自Philip Roberts的演讲《Help, I'm stuck in an event-loop》很是深入!
咱们把刚才了解的概念和图中作一个对应,上文中说到的浏览器为异步任务单独开辟的线程能够统一理解为WebAPIs,上文中说到的任务队列就是callback queue,咱们所说的主线程就是有虚线组成的那一部分,堆(heap)和栈(stack)共同组成了js主线程,函数的执行就是经过进栈和出栈实现的,好比图中有一个foo()函数,主线程把它推入栈中,在执行函数体时,发现还须要执行上面的那几个函数,因此又把这几个函数推入栈中,等到函数执行完,就让函数出栈。等到stack清空时,说明一个任务已经执行完了,这时就会从callback queue中寻找下一我的任务推入栈中(这个寻找的过程,叫作event loop,由于它老是循环的查找任务队列里是否还有任务)。
setTimeout(f1,0)是什么鬼
这个语句最大的疑问是,f1是否是马上执行?答案是不必定,由于要看主线程内的命令是否已经执行完了,以下代码:
setTimeout(function(){ console.log(1); },0); console.log(2);
界面渲染线程是单独开辟的线程,是否是DOM一变化,界面就马上从新渲染?
若是DOM一变化,界面就马上从新渲染,效率必然很低,因此浏览器的机制规定界面渲染线程和主线程是互斥的,主线程执行任务时,浏览器渲染线程处于挂起状态。
咱们已经知道,js一直是单线程执行的,浏览器为几个明显的耗时任务单独开辟线程解决耗时问题,可是js除了这几个明显的耗时问题外,可能咱们本身写的程序里面也会有耗时的函数,这种状况怎么处理呢?咱们确定不能本身开辟单独的线程,但咱们能够利用浏览器给咱们开放的这几个窗口,浏览器定时器线程和事件触发线程是好利用的,网络请求线程不适合咱们使用。下面咱们具体看一下:
假设耗时函数是f1,f1是f2的前置任务。
利用定时器触发线程
function f1(callback){ setTimeout(function(){ // f1 的代码 callback(); },0); } f1(f2);
这种写法的耦合度高。
利用事件触发线程
$f1.on('custom',f2); //这里绑定事件以jQuery写法为例 function f1(){ setTimeout(function(){ // f1的代码 $f1.trigger('custom'); },0); }
这种方法经过绑定自定义事件,对方法一解耦,这样能够经过绑定不一样的事件,实现不一样的回调函数,但若是应用这种方法过多,不利于阅读程序。
发布/订阅
上面"事件",彻底能够理解成"信号"。
咱们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其余任务能够向信号中心"订阅"(subscribe)这个信号,从而知道何时本身能够开始执行。这就叫作"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
首先,f2向"信号中心"jQuery订阅"done"信号。
jQuery.subscribe("done", f2);
而后,f1进行以下改写:
function f1(){ setTimeout(function () { // f1的任务代码 jQuery.publish("done"); }, 1000); }
jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引起f2的执行。
此外,f2完成执行后,也能够取消订阅(unsubscribe)。
jQuery.unsubscribe("done", f2);
这种方法的性质与"事件监听"相似,可是明显优于后者。由于咱们能够经过查看"消息中心",了解存在多少信号、每一个信号有多少订阅者,从而监控程序的运行。
参考:
https://www.cnblogs.com/woodyblog/p/6061671.html
http://www.ruanyifeng.com/blog/2014/10/event-loop.html