js执行是单线程的
单线程是指Js引擎执行Js时只分了一个线程给他执行,也就是执行js时是单线程的。说白点就是一个时间点只能做一件事,这里只是说javascript,浏览器可不是单线程的,浏览器可是多进程的
js引擎
JS引擎是浏览器的重要组成部分,主要用于读取并执行js。js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
各大浏览器的JS引擎:
浏览器 | Js引擎 |
---|---|
Chrome | V8 |
Firefox | SpiderMonkey |
IE | Chakra(查克拉) |
Safari | Nitro/JavaScript Core |
浏览器
浏览器(多进程)包含了Browser进程(浏览器的主进程)、第三方插件进程和GPU进程以及浏览器渲染进程,
- 浏览器进程
浏览器最核心的进程,负责管理各个标签页的创建和销毁、页面显示和功能(前进,后退,收藏等)、网络资源的管理,下载等。 - 插件进程
负责每个第三方插件的使用,每个第三方插件使用时候都会创建一个对应的进程、这可以避免第三方插件crash影响整个浏览器、也方便使用沙盒模型隔离插件进程,提高浏览器稳定性。 - GPU进程
负责3D绘制和硬件加速 - 渲染进程即 浏览器内核
浏览器会为每个窗口分配一个渲染进程,也就是我们常说的浏览器内核,这可以避免单个 page crash 影响整个浏览器。
其中浏览器渲染进程(多线程)和Web前端密切相关,包含以下线程
- GUI渲染线程
GUI 渲染线程负责渲染浏览器页面, 解析HTML元素, render DOM Tree,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。 - JS引擎线程
Javascript引擎,也可以称为JS内核,主要负责处理 Javascript 脚本程序,例如V8引擎。Javascript 引擎线程理所当然是负责解析 Javascript 脚本,运行代码。 - 事件触发线程(和EventLoop密切相关)
当一个事件被触发时该线程会把事件添加到事件队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。 - 定时器触发器线程
浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。 - 异步HTTP请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。
GUI渲染线程和JS引擎线程是互斥的,为了防止DOM渲染的不一致性,其中一个线程执行时另一个线程会被挂起。
那么既然 JavaScript 本身被设计为单线程,为何还会有像 WebWorker 这样的多线程 API 呢?我们来看一下 WebWorker 的核心特点就明白了?
- 创建 Worker 时, JS 引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
- JS 引擎线程与 worker 线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
执行栈
为了实现js执行时的单线程,js引擎维护一个执行栈(先进后出) FILO,(想象下桌子上的一摞书,最上面的先被拿走,最下面的后面才能拿走)(栈数据结构特点),主线程运行js代码时,会生成执行栈,能处理嵌套的函数,入栈出栈等操作。
宏任务和微任务
task queue任务队列是不是一个队列?
他是一个set 集合, 因为不是在取任务的时候不是像队列那般先进先出就完了, 而是先把最老的任务获取, 执行, 执行完毕之后才删除.
任务队列中分微任务和宏任务,且都是已经完成的异步操作. 这里要重点说明一下,宏任务并非全是异步任务,主代码块就是属于宏任务的一种!!XHR也是宏任务的一种如果当前的微任务没有执行完成时,是不会执行下一个宏任务的!
宏任务是每次执行栈执行的代码(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
浏览器为了能够使得JS引擎线程与GUI渲染线程有序切换,会在当前宏任务结束之后,下一个宏任务执行开始之前,对页面进行重新渲染(宏任务 > 微任务 > 渲染 > 宏任务 > 微任务 > 渲染…)
微任务是在当前宏任务执行结束之后立即执行的任务(在当前 宏任务执行之后,GUI渲染之前执行的任务)。微任务的响应速度相比setTimeout(下一个宏任务)会更快,因为无需等待UI渲染。
当前宏任务执行后,会将在它执行期间产生的所有微任务都执行一遍。
微任务的产生时机
主要有两种方式
- MutationObserver 监控某个DOM节点的变化, 绑定回调, 通过js来修改这个节点时(包括添加删除部分子节点)会将回调函数放入微任务队列中
- Promise 当调用Promise.resolve()或Promise.reject()时会产生微任务, 即对应then的第一个和第二个参数放入到微任务队列中
- 宏任务中的事件是由事件触发线程来维护的
- 微任务中的所有任务是由JS引擎线程维护的
下一个宏任务必然是在上一个微任务之后才会执行的
macroTask宏任务 | 浏览器 | node |
---|---|---|
I/O用户交互(点击, 拖动, 触摸,放大缩小) | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setImmediate | X | ✅ |
requestAnimationFrame | ✅ | X |
XMLHttpRequest | ✅ | ✅ |
js脚本执行 | ✅ | ✅ |
microTask微任务 | 浏览器 | node |
---|---|---|
process.nextTick | X | ✅ |
MutationObserver | ✅ | X |
Promise.then catch finally | ✅ | ✅ |
js执行机制
大概过程:
任务进入执行栈(js代码分同步任务和异步任务)
所有同步任务都在主线程执行。所有异步任务进入 EventTable(事件表),执行异步任务,当事件表中的异步任务执行完成之后,会在事件队列(event queue)中注册回调函数(函数移入event Queue)
主线程里的同步任务全部完成之后,会读取事件队列(event Queue)中的异步任务,进入主线程执行
js解析器会不断重复检查主线程执行栈是否为空,然后重复第三步,即Event Loop(事件循环)
js引擎存在 monitoring process进程,会持续不断的检查主线程执行栈是否为空
在事件循环中,每进行一次循环操作称为tick
JS引擎线程和事件触发线程
浏览器页面初次渲染完毕后,JS引擎线程结合事件触发线程的工作流程如下:
(1)同步任务在JS引擎线程(主线程)上执行,形成执行栈(Execution Context Stack)。
(2)主线程之外,事件触发线程管理着一个任务队列(Task Queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
(3)执行栈中的同步任务执行完毕,系统就会读取任务队列,如果有异步任务需要执行,将其添加到主线程的执行栈并执行相应的异步任务。
根据事件循环机制以及宏任务和微任务,重新梳理一下流程
- 执行一个宏任务(首次执行的主代码块 script 或者任务队列中的回调函数)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有任务(依次执行)
- JS引擎线程挂起,GUI线程执行渲染
- GUI线程渲染完毕后挂起,JS引擎线程执行任务队列中的下一个宏任务
结合Promise实例看看执行机制
1 | const first = () => (new Promise((resolve, reject) => { |
第一轮 事件循环,先执行宏任务,即主script,即new Promise立即执行,打印3 ,然后执行p这个new Promise,输出7,发现setTimeout,将回调函数放入下一轮宏任务队列中,p的then(then1),放入微任务队列,first的then(then2)放入微任务队列,执行打印4,第一轮tick执行结束
现在Event Queue中存在三个任务: 1个宏任务: setTimeout;2个微任务: then1.then2
执行微任务,先执行p.then打印1, 在执行first.then打印2
第一轮 事件循环结束,开始第二轮事件循环,先执行宏任务里面,打印5, resolve(6)不会生效,因为Promise状态一旦改变就不会发生变化,所以最终的打印顺序你都答对了吗
另外一个题目:
1 | function executor(resolve, reject) { |
again
1 | const promise = new Promise(resolve => { |
面试题之 了解v8引擎吗,一段js代码如何执行的(题外)
- 在执行一段代码时,JS 引擎会首先创建一个执行栈,JS引擎会创建一个全局执行上下文,并push到执行栈中, 这个过程JS引擎会为这段代码中所有变量分配内存并赋一个初始值(undefined),在创建完成后,JS引擎会进入执行阶段,这个过程JS引擎会逐行的执行代码,即为之前分配好内存的变量逐个赋值(真实值)。
- 如果这段代码中存在function的声明和调用,那么JS引擎会创建一个函数执行上下文,并push到执行栈中,其创建和执行过程跟全局执行上下文一样。但有特殊情况,即当函数中存在对其它函数的调用时,JS引擎会在父函数执行的过程中,将子函数的全局执行上下文push到执行栈,这也是为什么子函数能够访问到父函数内所声明的变量。
还有一种特殊情况是,在子函数执行的过程中,父函数已经return了,这种情况下,JS引擎会将父函数的上下文从执行栈中移除,与此同时,JS引擎会为还在执行的子函数上下文创建一个闭包,这个闭包里保存了父函数内声明的变量及其赋值,子函数仍然能够在其上下文中访问并使用这边变量/常量。当子函数执行完毕,JS引擎才会将子函数的上下文及闭包一并从执行栈中移除。 - last JS引擎是单线程的,那么它是如何处理高并发的呢?即当代码中存在异步调用时JS是如何执行的。比如setTimeout或fetch请求都是non-blocking的,当异步调用代码触发时,JS引擎会将需要异步执行的代码移出调用栈,直到等待到返回结果,JS引擎会立即将与之对应的回调函数push进任务队列中等待被调用,当调用(执行)栈中已经没有需要被执行的代码时,JS引擎会立刻将任务队列中的回调函数逐个push进调用栈并执行。这个过程我们也称之为事件循环。
总结
- 浏览器(多进程)包含了GPU进程(浏览器渲染进程),其中GPU进程(多线程)包含一下几个线程
1.GUI渲染线程
2.JS引擎线程
3.事件触发线程(和EventLoop密切相关)
4.定时触发器线程
5.异步HTTP请求线程 - javascript是一门单线程语言
- Event Loop是javascript的执行机制