一、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 引擎的运行机制