首先区分进程和线程
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
-
进程之间相互独立,但同一进程下的各个线程之间共享该进程的资源
-
调度和切换:线程上下文切换比进程上下文切换要快得多
-
总结 进程相当于一个工厂,线程相当于工厂里的工人,工厂之间互相独立,一个工厂可以有一个或者多个工人,同一个工厂的工人可以共享空间
浏览器内核
- 浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核本身(浏览器渲染进程)属于浏览器多进程中的一种。
- 浏览器内核本身有多种线程在工作。
- GUI 渲染线程:
- 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘(或者由于某种操作引起回流)都会调起该线程。
- 和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
- JS 引擎线程:
- 单线程工作,负责解析运行 JavaScript 脚本。
- 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
- 事件触发线程:
- 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
- 定时器触发线程:
- 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
- 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
-
http 请求线程:
- http 请求的时候会开启一条请求线程。
- 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
- GUI 渲染线程:
JavaScript 引擎是单线程
JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。
JavaScript 事件循环机制
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
-
JS 调用栈
当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。(JS 调用栈是一种后进先出的数据结构)
-
同步任务、异步任务
JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
-
Event Loop
调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
- 定时器
定时器会开启一条定时器触发线程来触发计时,定时器会在等待了指定的时间后将事件放入到任务队列中等待读取到主线程执行。
定时器指定的延时毫秒数其实并不准确,因为定时器只是在到了指定的时间时将事件放入到任务队列中,必须要等到同步的任务和现有的任务队列中的事件全部执行完成之后,才会去读取定时器的事件到主线程执行,中间可能会存在耗时比较久的任务,那么就不可能保证在指定的时间执行。
- 宏任务(macro-task)、微任务(micro-task)
除了广义的同步任务和异步任务,JavaScript 单线程中的任务可以细分为宏任务和微任务。
macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver。
- 从宏任务的头部取出一个任务执行;
- 执行过程中若遇到微任务则将其添加到微任务的队列中;
- 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则挨个儿出去执行,直到执行完毕;
- GUI 渲染;
- 回到步骤 1,执行第另外类型的宏任务(setTimeout, setInterval, setImmediate, I/O, UI rendering)直到宏任务执行完毕;
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
new Promise(function(resolve) {
console.log(3);
resolve(Date.now());
}).then(function() {
console.log(4);
});
console.log(5);
setTimeout(function() {
new Promise(function(resolve) {
console.log(6);
resolve(Date.now());
}).then(function() {
console.log(7);
});
}, 0);
执行步骤如下:
- 从宏任务的script(整体代码)头部执行 log(1),输出 1;
- 遇到 setTimeout,将回调的代码 log(2)添加到宏任务中等待执行;
- new Promise执行 console.log(3),将 then 中的 log(4)添加到微任务中;
- script(整体代码)执行 log(5),输出 5;
- 遇到 setTimeout,将回调的代码 log(6, 7)添加到宏任务中;
- 宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,存在一个微任务 log(4)(在步骤 3 中添加的),执行输出 4;
- 取出下一个宏任务setTimeout log(2)执行,输出 2;
- 宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,不存在;
- 取出下一个宏任务执行setTimeout(new Promise(function(resolve) {
})),执行 log(6),将 then 中的 log(7)添加到微任务中; - 宏任务执行完毕,存在一个微任务 log(7)(在步骤 9 中添加的),执行输出 7;
因此,最终的输出顺序为:1, 3, 5, 4, 2, 6, 7;