总所周知,javascript是一门依赖宿主环境的单线程的弱脚本语言,这意味着什么?javascript
宿主环境
(如浏览器、Node、Ringo等)和执行环境
(Javascript引擎V8,JavaScript Core等)共同构成;本文主要讲的就是第三点,从中引出下一个问题html
Javascript当初诞生的目的其实就是由于当年网络技术十分低效,如表单验证等个几十秒才能获得反馈的用户体验十分糟糕,为了给浏览器作些简单处理之前由服务器端负责的一些表单验证。被Netscape公司指派花了十天就负责设计出一门新语言的Javascript之父就是Brendan Eich。尽管他并不喜欢本身设计的这做品,就有了你们都听过的一句话:前端
"与其说我爱Javascript,不如说我恨它。它是C语言和Self语言一晚上情的产物。十八世纪英国文学家约翰逊博士说得好:'它的优秀之处并不是原创,它的原创之处并不优秀。'(the part that is good is not original, and the part that is original is not good.)"
做为浏览器脚本语言而诞生的JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只须要是单线程就足以解决目的,不然会带来很复杂的同步问题。可是没想到的是以后的网络愈加的发达,这些年来的浏览器大战为了争夺地盘,反而让Javascript被赋予了更多的职责跟可能性,今时今日的Javascript必须千方百计把自身的潜力激发出来,而单线程的弱点就被无限放大了,由于在阻塞任务的过程当中不必定是由于CPU被占用了,而多是由于I/O太慢(如AJAX请求,定时器任务,Dom事件交互等并不消耗CPU的等待形成资源时间浪费)。java
咱们一直都在说Javascript是单线程,但浏览器是多线程的,在内核控制下互相配合以保持同步,主要的常驻线程有:编程
好像铺垫的有点多,往外偏了,接下来往回拉一点谈谈这些怎么运行的。数组
本身画了一个丑丑的图,你们将就看着吧。promise
function addOne(n) { var x = n + 1; return addTwo(x); } function addTwo(n) { return n + 2; } console.log(addOne(1)) //4;
以这个例子作说明。
当调用addOne时建立一个包含addOne入参和局部变量的帧并添加进去stack,当调用到addTwo时也一样建立一个包含addTwo入参和局部变量的帧并添加进去在首部,执行完addTwo函数并返回时addTwo帧被移出stack,addOne执行完后addOne帧也被移除。
原理:当执行方法时都会创建本身的内存栈,在这个方法内定义的入参变量都会保存在栈内存里,执行结束后该方法的内存栈也将天然销毁了。浏览器
通常来讲,程序会划分有两种分配内存的空间 -- 堆(heap)
和栈(stack)
。服务器
内存空间 | 分配方式 | 结构 | 大小 | 存取速度 | 释放机制 |
---|---|---|---|---|---|
stack | 静态分配 | 有 | 小 | 快 | 随方法执行结束而销毁 |
heap | 动态分配 | 没有 | 大 | 慢 | 系统的垃圾回收机制销毁 |
由于栈只能存放下肯定大小的简单数据,因此像变量(其实也就是一个记录了指向复杂结构数据的地址指向,因此变量也是保存在栈里的)和基本类型Undefined、Null、Boolean、Number 和 String等是按值传递的都会保存在栈里,随着方法执行完毕而被销毁。
堆负责存放复杂结构的对象,数组,函数等建立成本较高而且可重用数据,即便方法执行完也不会被销毁,直到系统的垃圾回收机制核实了没有任何引用才会回收。
其实这只是栈的含义之一,Stack的三种含义网络
有时候咱们代码有问题致使栈堆溢出缘由大概是这种状况:
常见状况 | 可能状况 |
---|---|
栈溢出 | 无限递归死循环,递归越深层分配内存越多直至超过限制 |
堆溢出 | 循环生成复杂结构数据 |
好了,如今再看回上图,除了heap和stack以外还有一个。。。
Javascript里分两种队列:
console.log('log start!'); setTimeout(function () { console.log('setTimeout300'); }, 300) Promise.resolve().then(function () { console.log('promise resolve'); }).then(function () { console.log('promise resolve then'); }) new Promise(function (resolve, reject) { console.log('promise pending'); resolve(); }).then(function () { console.log('promise pending then'); }) setTimeout(function () { console.log('setTimeout0'); Promise.resolve().then(function () { console.log('promise3 in setTimeout'); }) }, 0) console.log('log end!'); // log start! // promise pending // log end! // promise resolve // promise pending then // promise resolve then // setTimeout0 // promise3 in setTimeout // setTimeout300
例子过程,具体分析下面再说。
第一次执行事件打印:log start!, promise pending, log end!, promise resolve,promise pending then,promise resolve then;
第二次执行事件打印:setTimeout0,promise3 in setTimeout;
第三次执行事件打印:setTimeout300;
下面终于开始走到正题了
我在上面铺垫了这么多东西,你们大概都能有个初步印象,而后所谓的Event Loop就是把这些东西串联起来的一种机制吧,由于这东西各有理解,好比两位前端大牛之间就有分歧。
阮一峰:JavaScript 运行机制详解:再谈Event Loop
朴灵:朴灵评注
我看过他们不少的博客和书籍,对我帮助都很大,我就用本身的见解讲讲我眼中的Event Loop。
1,全部的任务都被放主线程上运行造成一个 执行栈(execution context stack),其中的方法入参变量保存在栈内存中,复杂结构对象被保存在堆内存中;
2,同步任务直接执行并阻塞后续任务等待结束,其中遇到一些异步任务会新开线程去执行该任务(如上面提到的定时器触发线程,异步http请求线程等)而后往下执行,异步任务执行完返回结果以后就把回调事件加入到任务队列(Queue)
;
3,当执行栈(execution context stack)
全部任务执行完以后,会到任务队列(Queue)
里提取全部的微任务队列(micro tasks)
事件执行完;
4,一次循环结束,GUI渲染线程接管检查,从新渲染界面;
5,执行栈(execution context stack)
到宏任务队列(macro tasks)
提取一个事件到执行,接着主线程就一直重复第3步;
大概理解就这样子,固然可能会有点误差,欢迎指正!
我在上面线程说过
定时器触发线程:由于JS引擎是单线程容易阻塞,因此须要有单独线程为setTimeout
和setInterval
计时并触发,一样是符合触发条件(记时完毕)被触发时会把对应任务添加处处理队列的尾部等到JS引擎空闲时处理;W3C标准规定时间间隔低于4ms被算为4ms。
里面有一些须要特别注意的地方:
1,计时完毕只是把对应任务添加处处理队列,依然要等执行栈空闲才会去提取队列执行,这个概念很重要,切记!即便设置0秒也不会立马执行,由于W3C标准规定时间间隔低于4ms被算为4ms,具体看浏览器,我我的认为无论怎样始终都会被放置处处理队列等待处理;
2,setTimeout重复执行过程当中每次时间偏差会影响后续执行时间,而setInterval是每次精确时间执行,固然这是指他们把对应任务添加处处理队列的精确性;
可是setInterval也有一些问题:
坦白讲,我本来时打算写一篇关于异步编程的文章,而后在铺垫前文的路上拉不回来了就变成了一篇梳理Javascript执行机制了,不过不要紧,理解这些也是很重要的