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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});

}));

first().then((arg) => {
console.log(arg);
});
console.log(4);

第一轮 事件循环,先执行宏任务,即主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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function executor(resolve, reject) {
let rand = Math.random()
console.log(1, rand);
if(rand > 0.5) resolve()
reject(rand)
}
new Promise(executor).then(_ => {
console.log('success-0')
return new Promise(executor)
}).then(_=> {
console.log('success-1')
return new Promise(executor)
}).catch(er =>{
console.log('error', er)
})
console.log(2)

again

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const promise = new Promise(resolve => {
console.log("11111");
setTimeout(() => {
console.log("22222");
}, 0);
resolve();
console.log("resolve");
throw new Error("error"); // 注意这里是throw!!!
console.log("error");
});
promise
.then(
() => {
console.log("33333");
setTimeout(() => {
console.log("44444");
}, 0);
},
() => {
console.log("reject");
}
)
.catch(() => {
console.log("catch");
});
console.log("55555");
// answer
11111
VM1014:7 resolve
VM1014:26 55555
VM1014:14 33333
undefined
VM1014:4 22222
VM1014:16 44444

面试题之 了解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的执行机制

参考链接