Javascript执行机制--单线程,同异步任务,事件循环

总所周知,javascript是一门依赖宿主环境的单线程的弱脚本语言,这意味着什么?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执行线程

咱们一直都在说Javascript是单线程,但浏览器是多线程的,在内核控制下互相配合以保持同步,主要的常驻线程有:编程

  • GUI渲染线程:负责渲染界面,解析HTML,CSS,构建DOM和Render树布局绘制等。若是过程当中遇到JS引擎执行会被挂起线程,GUI更新保存在一个队列中等待JS引擎空闲才执行;
  • JS引擎线程:负责解析运行Javascript;执行时间过程会致使页面渲染加载阻塞;
  • 事件触发线程,浏览器用以控制事件循环。当JS引擎执行过程当中触发的事件(如点击,请求等)会将对应任务添加到事件线程中,而当对应的事件符合触发条件被触发时会把对应任务添加处处理队列的尾部等到JS引擎空闲时处理;
  • 定时器触发线程:由于JS引擎是单线程容易阻塞,因此须要有单独线程为setTimeout和setInterval计时并触发,一样是符合触发条件(记时完毕)被触发时会把对应任务添加处处理队列的尾部等到JS引擎空闲时处理;W3C标准规定时间间隔低于4ms被算为4ms。
  • 异步http请求线程:XMLHttpRequest在链接后浏览器新开线程去请求,检测到状态变化若是有设置回调函数会产生状态变动事件,而后把对应任务添加处处理队列的尾部等到JS引擎空闲时处理;

好像铺垫的有点多,往外偏了,接下来往回拉一点谈谈这些怎么运行的。数组

什么是堆(heap)和栈(stack)?

本身画了一个丑丑的图,你们将就看着吧。
图片描述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以外还有一个。。。

什么是Queue(任务队列)?

Javascript里分两种队列:

  • 宏任务队列(macro tasks):事件循环中能够有多个macro tasks,每次循环只会提取一个,包括script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering等.
  • 微任务队列(micro tasks):事件循环中只有一个而且有优先级区别micro tasks,每次循环会提取屡次直至队列清空,包括process.nextTick, Promise, Object.observer, MutationObserver等.
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)!

我在上面铺垫了这么多东西,你们大概都能有个初步印象,而后所谓的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引擎是单线程容易阻塞,因此须要有单独线程为 setTimeoutsetInterval计时并触发,一样是符合触发条件(记时完毕)被触发时会把对应任务添加处处理队列的尾部等到JS引擎空闲时处理;W3C标准规定时间间隔低于4ms被算为4ms。

里面有一些须要特别注意的地方:
1,计时完毕只是把对应任务添加处处理队列,依然要等执行栈空闲才会去提取队列执行,这个概念很重要,切记!即便设置0秒也不会立马执行,由于W3C标准规定时间间隔低于4ms被算为4ms,具体看浏览器,我我的认为无论怎样始终都会被放置处处理队列等待处理;
2,setTimeout重复执行过程当中每次时间偏差会影响后续执行时间,而setInterval是每次精确时间执行,固然这是指他们把对应任务添加处处理队列的精确性;

可是setInterval也有一些问题:

  • 累计效应,若是执行栈阻塞时间足够长以致于队列中已经存在多个setInterval的对应任务的状况,执行时间会远低于开发者指望的结果;
  • 部分浏览器(如Safari等)滚动过程不执行JS,容易形成卡顿和未知错误;
  • 浏览器最小化显示时setInterval会继续执行,可是对应任务会等到浏览器还原再一瞬间所有执行;

结语

坦白讲,我本来时打算写一篇关于异步编程的文章,而后在铺垫前文的路上拉不回来了就变成了一篇梳理Javascript执行机制了,不过不要紧,理解这些也是很重要的

相关文章
相关标签/搜索