一、JavaScript预解析
JavaScript代码运行分为两个阶段:
- (1) 预解析
所有函数定义提前,函数体晋升(当然不包含如var box = function() {} )
形参申明并赋值
变量申明(不赋值)
- (2) 执行
依照js运行机制从,从上到下执行
二、过程与线程
- 过程是cpu资源分配的最小单位(是可能领有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建设在过程的根底上的一次程序运行单位,一个过程能够有多个线程
举例:此处有多个工厂,每个工厂有1个或多个工人。此时工厂就好比过程,有独自专属本人的工厂资源;工人就好比是线程,多个工人在工厂中写作工作。工厂的空间是工人们共享的,这象征一个过程的内存空间是共享的,每个线程都能够共享内存。并且每个工厂之间互相独立存在。
- 应用程序必须运行在某个过程的某个线程上
- 一个过程至多有一个运行的线程:主线程,过程启动后主动创立
三、浏览器过程
浏览器内核是指反对浏览器运行的最外围的局部,分为渲染引擎和JS引擎。当初JS引擎比拟独立,内核更加偏向于说渲染引擎
(1)浏览器内核分类
- Chrome、Safari: Webkit (Bink)
- Firefox:Gecko
- IE:Trident
- 360、搜狗等国内浏览器:Trident+Webkit
- ...
(2)浏览器过程
- 浏览器是多过程的
- 浏览器之所以能运行,是因为零碎给它的过程调配了资源(cpu、内存)
- 简略来说,每打一个Tab页,就相当于创立了一个独立的浏览器过程
浏览器过程的组成:
- Browser过程
浏览器的主过程,负责协调、主控,只有一个。
负责内容:浏览器页面显示;与用户交互(后退、后退等);网络资源的治理、下载;各个页面的治理,创立和销毁其余过程等
- 第三方插件过程
每种类型的插件对应一个过程,仅当插件应用时才创立 - GPU过程
最多一个,用于3D绘制等 - 浏览器渲染过程(浏览器内核,Renderer过程,外部是多线程的)
默认 每个Tab页面一个过程,互不影响
负责内容:页面渲染;脚本执行;事件处理
浏览器是多线程的劣势:防止单个Tab页解体或单个插件解体影响其余整个浏览器,能够充沛多核优势,方便使用沙盒模型隔离插件等过程,进步浏览器的稳定性。毛病是,内存和cpu耗费会更大,有点空间换工夫的意思。
Borwser过程与浏览器内核(Renderer过程)的通信过程:
Browser过程收到用户申请,首先须要获取页面内容(譬如通过网络下载资源),随后将该工作通过RendererHost接口传递给Render过程
- 渲染线程接管申请,加载网页并渲染网页,这其中可能须要Browser过程获取资源和GPU过程来帮忙渲染
- 当然可能会有JS线程操作DOM(可能会造成回流并重绘)
- 最初Renderer过程将后果传递给Browser过程
- Renderer过程的Renderer接口收到音讯,简略解释后,交给渲染线程,而后开始渲染
- Browser过程收到后果并将后果绘制进去
四、浏览器渲染过程
对于前端操作来说 ,最重要的是渲染过程,并且渲染过程也是多线程的。
渲染过程蕴含哪些线程?
GUI渲染线程
- 负责渲染浏览器页面,解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制等
- 负责重绘(Repaint)和回流(Reflow)
- GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI线程会保留在一个队列里等 js引擎闲暇时执行。
JS引擎线程
- 负责解决JavaScript脚本,执行代码
事件触发线程
- 次要负责将筹备好的事件交给JS引擎线程执行
比方setTimeout定时器计数完结、ajax等异步申请胜利并触发回调函数、用户触发点击事件等,该线程会将整装待发的事件退出到工作队列的队尾,期待JS引擎线程的执行。
定时器触发线程
- 次要负责异步定时器一类的函数解决,如setTimeout、setInterval
主线程顺次执行代码时,遇到定时器,会将定时器交给该线程解决。当计数结束后,事件触发线程会将计数结束的事件退出到工作队列的尾部,期待JS引擎线程执行。
异步HTTP申请线程
- 负责执行异步申请一类的函数,如:ajax、axios、promise等
主线程顺次执行代码是,遇到异步申请,会将异步申请函数交给该线程解决。当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数退出到工作队列的尾部,期待 JS引擎线程执行。
五、事件循环
1 浏览器中的事件循环
JavaScript语言是单线程的,意思是同一时间只能做一件事。起初为了无效利用多核CPU的计算能力,HTML5提出Web Server规范,容许JavaScript脚本创立多个线程,然而子线程齐全受主线程管制,并且子线程不能操作DOM。所以新规范并没有扭转JavaScript单线程的实质。
简略形容JS的执行机制:
- 首先判断JS是同步工作还是异步工作,同步工作就进入主线程执行,异步工作进入event table
- 异步工作在event table中注册函数,异步函数又分为宏工作(macro-task)和微工作(micro-task),当满足触发条件后,宏工作被推入宏工作队列(macro-task queue),微工作被推入微工作队列(micro-task queue)
- 同步工作在主线程中始终执行,直到同步工作执行结束,主线程闲暇闲暇时,才去微工作队列(micro-task queue)中查看是否有可执行的异步工作,如果有就推入主线程中执行
- 直到全副微工作顺次执行结束后,主线程闲暇,再去宏工作队列(macro-task queue) 查看是否有可执行的异步工作,如果有就推入主线程中执行
以上四步循环执行,就是event loop。
一个残缺的Event Loop过程:
① 所有的同步工作都在主线程上执行,造成一个执行栈(exection context stack),咱们能够认为执行栈是一个函数调用的栈构造,遵循先进后出的准则。除了主线程的执行栈,还存在一个工作队列(task queue),工作队列分为宏工作队列(macro-task queue)和微工作队列(micro-task queue)。
一开始执行栈为空,宏工作队列(macro-task queue)里只有一个script代码(整体代码),微工作队列(micro-task queue)队列为空。
② 宏工作队列(macro-task queue)中的全局上下文(script标签)会被推入执行栈,同步代码执行。在执行的过程中会判断是同步工作还是异步工作,同步工作顺次执行,异步工作会通过对一些接口的调用而产生新的macro-task和micro-task(只有异步工作有了运行后果,就会在对应的工作队列中搁置一个事件,期待调用)。同步代码执行完了,script脚本会行和出队的过程。
③ 上一步出队的是一个macro-task,这一步要解决的是micro-task。须要留神的是,当macro-task出队时,工作是一个一个执行的,而micro-task出队时,工作是一队一队执行的。因而,咱们解决micro-task这一步,会一一执行队列中的工作并把它出队,直到队列被清空。
④ 执行渲染操作,更新页面
⑤ 查看是否存在Web worker工作,如果有,则对其进行进行解决
⑥ 上述过程反复循环,直到两个队列都清空
宏工作队列能够有多个,而微工作队列只有一个:
- 常见的macro-task:setTimeout、setInterval、script(整套代码)、I/O操作、UI渲染等;
- 常见的micro-task:new Promise().then(回调)、process.nextTick、MutationObserver(HTML5新个性)等
2 Node中的事件循环
Node中的事件循环与浏览器的是齐全不同不同的货色。Node采纳V8作为js的解析引擎,而I/O解决方面应用本人设计的libuv。
libuv是一个基于事件驱动的跨平台形象层,封装了不同操作系统的一些底层个性,对外提供API,事件循环也是在它外面实现:
NodeJS运行机制如下:
- V8引擎解析JavaScript脚本
- 解析后的代码调用Node API
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,造成Event Loop(事件循环),以异步的形式将工作的执行后果返回给V8引擎
- V8引擎再将后果返回给用户
libuv引擎的事件循环分为6个阶段:
- timers阶段:执行timers(setTimeout和setInterval)的回调
- I/O callbacks阶段:解决上一轮循环多数未执行的的I/O回调
- idel、prepare阶段:仅Node外部应用
- poll阶段:获取新的I/O事件,执行I/O回调
- check阶段:执行setImmediate()回调
- close callbacks阶段:执行socket的close事件回调
绝大部分的异步工作都在timers、poll、check这个3个阶段解决
NodeJS执行环境下的非凡状况:
1)setTimeout和setImmediate
二者十分类似,区别次要在于调用机会不同:
- setImmediate设计在poll阶段实现时执行,即check阶段
- setTimeout设计在poll阶段为闲暇时,且设定阶段达到后执行,但它在timers阶段执行
setTimeout(function timeout () { console.log('timeout');},0);setImmediate(function immediate () { console.log('immediate');});
对于以上代码,setTimeout可能执行在前,也可能执行在后;
取决于setImmediate的筹备工夫;因为当setTimeout指定工夫小于4ms,则减少到4ms(4ms是H5de新规范,2010年以前的浏览器是10ms)
然而如果二者在I/O callback外部回调时,总是先执行setImmediate,后执行setTimeout:
const fs = require('fs')fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0) setImmediate(() => { console.log('immediate') })})// immediate// timeout// 因为这两个代码都写在I/O回调中,I/O回调是在poll阶段执行,当回调执行结束后队列清空,发现SetImmediate回调,所以立刻跳转到check阶段执行回调。});
2)process.nextTick
process.nextTick是独立于Event Loop之外的,它有一个本人的队列,会优先于其余micro-task队列执行:
setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') }) }) })})// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
3 浏览器与Node的Event Loop差别
浏览器环境下,micro-task的工作队列是每个macro-task执行之后执行;
Node环境下,在node10及其以前版本,micro-task会在事件循环的各个阶段之间执行,也就是一个阶段执行结束,就会执行micro-task队列的工作
Node在node11版本开始,Event Loop的运行原来产生了变动,一旦一个阶段里的宏工作执行完,就会立刻执行微工作队列,这一点与浏览器始终。
4 Web worker
因为JS是单线程,当遇到计算密集型或高提早的工作,用户界面可能会短暂“解冻”,不能做其余操作。
于是HTML5提出Web Worker,它容许JavaScript发明多线程环境,容许主线程创立Worker线程,将一些任务分配给后者。主线程运行的同时,Worker线程在后盾运行,两者互不烦扰,等到Worker实现计算工作,在把后果返回给主线程。
Web Worker的长处是能够承当一些密集型或高提早工作,使主线程晦涩,不被阻塞或拖慢。
毛病:
- 不能跨域加载JS
- Worker外部代码不能拜访DOM
- 不是所有浏览器都反对这个新个性
Web Worker应用办法:
主线程调用Worker线程:
- 主线程通过new Worker()调用Worker构造函数,新建一个Worker线程
- 主线程调用worker.postMessage()办法,向Worker发消息
- 主线程通过worker.onmessage指定监听函数,接管子线程发回来的音讯
// 主线程:var input = document.getElementById('number')document.getElementById('btn').onclick = function () { var number = input.value //1、创立一个Worker对象 var worker = new Worker('worker.js') // 3、绑定接管音讯的监听 worker.onmessage = function (event) { console.log('主线程接管分线程返回的数据: '+event.data) alert(event.data) } // 2、向分线程发送音讯 worker.postMessage(number) console.log('主线程向分线程发送数据: '+number)}console.log(this) // window
Worker线程响应:
- Worker外部通过onmseeage()监听事件
- 通过postMessage(data)办法向主线程发送数据
//worker.js文件function fibonacci(n) { return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用}console.log(this)//[object DedicatedWorkerGlobalScope]this.onmessage = function (event) { var number = event.data console.log('分线程接管到主线程发送的数据: '+number) //计算 var result = fibonacci(number) postMessage(result) console.log('分线程向主线程返回数据: '+result) // alert(result) alert是window的办法, 在分线程不能调用 // 分线程中的全局对象不再是window, 所以在分线程中不可能更新界面}
参考资料:
https://github.com/ljianshu/B...
https://juejin.im/post/5bb054...
深入浅出JavaScript运行机制
10分钟了解JS引擎的执行机制
浏览器组成
全面梳理JS引擎的运行机制