前置知识
JS 运行机制
JS 是单线程的,在同一时间只能做同一件事:协调事件、用户交互、脚本、UI渲染和网络处理等行为。
JS 为什么是单线程:因为 JS 可以操作 DOM,那如果是多个线程操作同一个 DOM,那如何判断优先级呢?所以为了避免这个问题,JS 就只能是单线程的
JS 的代码是一行行执行的,即同步执行
执行时会创建执行上下文,主要有全局、函数执行上下文
遇到函数被调用则创建一个新的函数执行上下文,函数的代码在里面被执行
那遇到异步任务时,不可能一直等待异步任务完成后再执行,因此会放入异步任务处理模块中执行,等其执行完毕后再将其回调函数放到事件队列中,然后由事件循环来处理(需要执行的就拿出来执行)
异步任务分为:微任务、宏任务,微任务优先级高于宏任务
常见的微任务包括 Promise 的 then 方法和 async/await 中的 await,宏任务包括定时器、事件处理、网络请求等
所以整个 JS 运行是由:[同步任务、异步任务(微任务、宏任务)] 不停的循环直到执行完毕
事件循环(Event Loop)
- 第一次进入
- 遇到同步代码,立即执行
- 遇到异步代码放入异步任务处理模块中执行
-
- 执行完成后,若为微任务,将其回调事件放入事件队列-微任务队列中
- 执行完成后,若为宏任务,将其回调事件放入事件队列-宏任务队列中
- 执行本轮的所有同步代码,直到执行完毕
- 进入微任务队列中,执行所有的微任务,直至清空所有的微任务
- 按顺序拿出一个宏任务,执行该宏任务,开始下一轮事件循环(重复步骤2)
进程、线程
- 进程(Process) : 进程是计算机中的一个独立执行单位。一个进程可以包括多个线程,每个线程都在进程的上下文中运行。不同的进程之间是相互独立的,它们拥有各自的内存空间和资源。
- 线程(Thread) : 线程是进程内的执行单元,它可以看作是进程的一个子任务。一个进程可以包含多个线程,这些线程共享进程的内存和资源,因此它们之间可以更容易地通信和协作。
大白话:一个餐厅就是一个进程,餐厅里面的厨师、端菜、收银等是线程
浏览器原理
浏览器进程、线程
浏览器渲染原理
浏览器的渲染进程专门来负责将HTML、CSS、JavaScript 转为可视化的页面。
要讲浏览器的渲染原理,那就不得不从源头讲起:浏览器地址栏输入地址到页面渲染,发生了什么?
a. 输入地址后,浏览器开始解析域名,通过 DNS(Domain Name System) 查询到对应的 IP(Internet Protocol)。
-
- 那为什么是 IP?
-
-
- 因为对计算机来说,IP 才是它能理解的。
-
-
- 那为什么要找 IP 呢?
-
-
- 因为我们的静态资源(HTML/CSS/JS/其他资源等)一定是放在某个服务器上的,而对计算机来说, 精确的找到服务器就需要 IP
-
-
- 域名与 IP 是啥关系?
-
-
- 域名(英语:Domain Name),是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位
- IP地址:是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。是一种在Internet上的给主机编址的方式,为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
- IP地址和域名是一一对应的,这份域名地址的信息存放在一个叫域名服务器(DNS,Domain name server)的主机内,使用者只需了解易记的域名地址,其对应转换工作就留给了域名服务器。域名服务器就是提供IP地址和域名之间的转换服务的服务器。
- 域名的 IP 的别名,一个更容易记得别名
-
-
- 域名的组成?举例:www.example.com/path/to/fil…
-
-
- 协议(Protocol) :表示使用的通信协议,常见 HTTP、HTTPS;例子中的协议为:http:。浏览器中为
location.protocol - 域名(Domain Name) :是网站在互联网上的唯一标识。例子中的域名为:www.example.com,其中
.com是顶级域名,example是主域名,www是子域名。浏览器中为location.host - 端口号(Port) :访问网站的端口号,默认为80(可省略)。例子中的端口为:无,因为例子中没有明确展示端口。浏览器中为
location.port - 路径(Path) :表示网站上具体的文件或目录路径。例子中的路径为:path/to/file。浏览器中为
location.pathname - 参数(Query) :表示向服务器传递的参数,用于定制请求的内容。查询参数以"?"开头,多个参数之间使用"&"分隔。例子中的参数为:空。浏览器中为
location.search - 锚点(Anchor) :表示网页内部的定位点。锚点以"#"开头,用于跳转到网页的特定位置。例子中的锚点为:#section1。浏览器中为
location.hash
- 协议(Protocol) :表示使用的通信协议,常见 HTTP、HTTPS;例子中的协议为:http:。浏览器中为
-
b. 找到 IP 后,采用TCP(Transmission Control Protocol,传输控制协议)三次握手来确认连接
-
- TCP 三次握手:
-
-
- 第一次:浏览器向服务器发送一个 SYNC 消息,要求进行同步(同步意味着连接)
- 第二次:服务器将回复一个 SYNC-ACK 消息,由 SYNChronization(要求进行同步) 和 ACKnowledgement(确认消息) 组成
- 第三次:浏览器回复 ACK 消息
-
-
- TCP 三次握手的情景模拟
-
-
- 发送方:老铁,可以听得到我说话吗,老铁。
- 接收方:可以听到,你听得到吗?
- 发送方:听到了,那我开始说正事了。
-
c. 针对 HTTPs 协议的,还需要 TLS(Transport Layer Security,传输层安全) 协商握手,一种加密协议,用于确保通信的安全。
-
- TLS 握手的情景解释:发送方与接收方互发消息,然后让第三方参与见证,最终互相协商出一个本次会话的暗号。首先确保互相是互相要找的那个人,其次确保本次会话安全。
d. 获取资源,在建立连接后,浏览器会发起一个初始的 HTTP GET 请求,用于获取 HTML 文件。
-
- HTTP(HyperText Transfer Protocol,超文本传输协议) 请求组成
-
-
- 请求:请求行、请求头、请求体
- 请求:请求行、请求头、请求体
-
-
-
-
- 请求行包含:请求方法、请求 URL、HTTP协议与版本
-
-
-
-
-
-
- 请求方法:GET、POST、DELETE、PUT、等
-
-
-
-
-
-
-
-
- GET、POST 的区别
-
-
-
-
-
-
-
-
-
-
- 参数:
-
-
-
-
-
-
-
-
-
-
-
-
- GET 的放在请求 URL 后面,不安全,并且 URL 的长度会限制参数大小,没有请求体
- POST 的放在请求体,较安全,请求数据大小没限制
-
-
-
-
-
-
-
-
-
-
-
-
- 缓存:GET 请求可以被缓存,POST 请求不会被缓存
-
-
-
-
-
-
-
-
-
-
- 常见 GET 请求:地址栏直接访问、
<a href="xx">、<img src="xx">等
- 常见 GET 请求:地址栏直接访问、
-
-
-
-
-
-
-
- 请求头:通常以键值对 key:value 方式传递数据。
-
-
-
-
-
-
- 常见的有:
-
-
-
-
-
-
-
-
- Authorization: xxxxx
- Referer:表示这个请求是从哪个 url 跳过来的,直接访问的话就没有
-
-
-
-
-
-
-
-
-
-
Referer:https://www.bing.com/
-
-
-
-
-
-
-
-
-
-
- Accept:告诉服务端,该请求所能支持的响应数据类型
-
-
-
-
-
-
-
-
-
-
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
-
-
-
-
-
-
-
-
-
-
- Cookie:给服务器用的
-
-
-
-
-
-
-
-
-
-
Cookie: JSESSIONID=15982C27F7507C7FDAF0F97161F634B5
-
-
-
-
-
-
-
-
-
-
- User-Agent:浏览器通知服务器,客户端浏览器与操作系统相关信息
-
-
-
-
-
-
-
-
-
-
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0
-
-
-
-
-
-
-
-
- 请求体:存放 POST/PUT 请求的参数
-
-
-
-
- 响应:响应行、响应头、响应体
- 响应:响应行、响应头、响应体
-
-
-
-
- 响应行包含:HTTP协议与版本、状态码与描述
-
-
-
-
-
-
- 状态码:是由三位数组成,定义响应状态,第一位决定响应类别
-
-
-
-
-
-
-
-
- 1xx:指示信息,表示请求已接收,继续处理
- 2xx:成功,表示请求已被成功接受与处理
-
-
-
-
-
-
-
-
-
-
- 200 OK:请求成功
- 204 No Content:服务器处理成功,无返回内容
-
-
-
-
-
-
-
-
-
-
- 3xx:重定向
-
-
-
-
-
-
-
-
-
-
- 301:永久重定向
- 302:临时重定向
-
-
-
-
-
-
-
-
-
-
- 4xx:客户端报错
-
-
-
-
-
-
-
-
-
-
- 400:客户端请求有语法错误
- 401:客户端请求未经授权
- 403:服务器收到请求,但拒绝提供服务
- 404:请求资源不存在
-
-
-
-
-
-
-
-
-
-
- 5xx:服务端报错
-
-
-
-
-
-
-
-
-
-
- 500:服务器发生错误
- 503:服务器当前不能处理客户端的请求
-
-
-
-
-
-
-
-
- 响应头:通常以键值对 key:value 方式传递数据,服务器通过响应头来控制浏览器的行为,不同的头浏览器操作不同
- 响应体:服务器发给浏览器的数据,根据不同的 Content-Type,对应的数据类型也不一样
- 响应头:通常以键值对 key:value 方式传递数据,服务器通过响应头来控制浏览器的行为,不同的头浏览器操作不同
-
-
e. 解析 HTML,生成 DOM 树
当浏览器接受到服务器返回的 HTML 文件后,浏览器引擎开始 HTML 解析
-
- 浏览器引擎(不要与浏览器 JavaScript 引擎混淆哦)
-
-
- 浏览器引擎是每个主要浏览器的核心组件,它的主要作用是结合结构 (HTML) 和样式 (CSS),以便它可以在我们的屏幕上绘制网页。
- 常见浏览器引擎:
-
-
-
-
- Webkit:由 Apple 为 Safari 开发,但在 iOS 上,包括 Firefox 和 Chrome 在内的所有浏览器也由 WebKit 提供支持,使用 C++ 书写的
- Gecko:由 Mozilla 为 Firefox 开发,目前仅少数浏览器(Firefox)还在使用它,使用 C++ 和 JavaScript 编写的,自 2016 年起,还用 Rust 编写。
- Blink:由 Google 为 Chrome 开发,是 Webkit 的一个分支,使用 C++ 书写的
- 可以通过
navigator.userAgent查看
-
-
-
-
-
-
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0- 其中的
AppleWebKit代表浏览器引擎
-
-
-
-
- HTML 解析步骤:词法解析与树构造
-
-
- 词法解析:将服务器返回的 HTML 源代码(不是的话就要解析为 HTML 源代码),最后解析为令牌流(Token),这个流可以被进一步用于构建抽象语法树(AST)等后续解析和处理步骤
- 树构造:DOM(Document Object Model,文档对象模型),DOM 树用于描述 HTML 文档的内容
- 词法解析:将服务器返回的 HTML 源代码(不是的话就要解析为 HTML 源代码),最后解析为令牌流(Token),这个流可以被进一步用于构建抽象语法树(AST)等后续解析和处理步骤
-
-
-
-
- 从根节点
<html>开始构建
- 从根节点
-
-
-
-
- 解析器是从上到下逐行工作。
-
-
-
-
- 当解析器遇到非阻塞资源(例如图像)时,浏览器会向服务器请求这些图像并继续解析。
- 另一方面,如果它遇到阻塞资源(CSS 样式表、在 HTML 的 部分添加的 Javascrpt 文件或从 CDN 添加的字体),解析器将停止执行,直到所有这些阻塞资源都被下载
-
-
f. 解析 CSS,生成 CSSOM 树
- 在解析 HTML 时,遇到 CSS 后,就开始解析 CSS,并生成 CSSOM 树
- CSS 解析也是从 词法解析 => 生成源代码 => 生成 Tokens => 构建节点 => 生成 CSSOM 树
- CSS 规则是从右到左阅读的,这样的代码:
section p { color: blue; }, 浏览器将首先查找页面上的所有 p 标签,然后它会查看这些 p 标签中是否有一个 section 标签作为父标签。
g. 执行 JS,生成 AST 树
浏览器获取 Javascript 文件后,代码被解释、编译、解析和执行。
JS 引擎
是一种在浏览器中执行 JS 代码的软件,用来将 JS 代码翻译为计算机可以理解的东西。
由浏览器提供,不同浏览器供应商,有不同的 JS 引擎
- V8:Google 浏览器提供的 JS 引擎,由 C++ 编写
- JavaScriptCore:Safari 浏览器的 Webkit 内置的 JS 引擎
- Chakra:Edge 浏览器的 JS 引擎,由 C++ 编写
- SpiderMonkey:FireFox 浏览器的 JS 引擎,由 C++、Javascript 和 Rust 编写
额外知识:编译、解释、即时编译
编译:将 JS 代码一次性转换为机器代码,并创建一个目标文件,该文件可以在任何机器上运行
解释:逐行检查 JS 代码并立即执行。JS 是解释型语言(不需要编译)
即时编译:JS 代码在执行时(在运行时)被编译,目前大多数浏览器都是用它来运行 JS 代码
JS 代码是如何处理的?
- JS 代码进入到 JS 引擎后,开始逐行解析并转为 AST(Abstract Syntax Tree,抽象语法树) 的数据结构
-
- JS 转 AST 工具:AST explorer
- JS 转 AST 工具:AST explorer
- 构建 AST 后,然后使用即时编译来执行代码
h. 创建可访问(无障碍)树
可访问性指:尽可能开发处易于访问的内容,无论个人的身体和认知能力以及他们如何访问网络 (ACT-Accessibility Conformance Testing)
ACT:专门测试与评估是否符合无障碍标准的方法
可访问树:是基于 DOM 创建的,在 DOM 树基础上增加一些额外信息,确保更好的无障碍体验,并且使无障碍辅助技术(屏幕辅助阅读、放大镜等)能更好的解释页面内容,并且可访问树与 DOM 树是保持同步更新的
增强可访问性的方法:语义化 HTML、键盘导航、适当描述(alt)等
i. 创建渲染树,并显示到页面上
渲染树是确保页面内容以正确的顺序绘制元素,由 DOM 与 CSSOM 结合而成。
如何创建渲染树呢?
从 DOM 树根节点,遍历可见节点,并在 CSSOM 树里面找到对应规则,最终结合为渲染树(一个包含所有可见节点、内容和样式的树,但不包含节点的尺寸与位置)
渲染树创建完后,进行 Layout(布局)
从渲染树根节点开始,基于设备视口计算每个节点的尺寸与位置。
每次更改节点的尺寸与位置都会触发布局,也就是重排
Layout 后,进行绘制
当 Layout 完后,就可以在屏幕上绘制节点了。
每次更改节点的样式都会触发绘制,也就是重绘
浏览器 tab
浏览器不同 Tab 之间是属于进程,那若想进行通信,可以使用如下方式:
- localStorage
-
- 基于同源+事件监听
- cookie
-
- 基于同源+定时器
- websocket
-
- 基于后端服务实现,提供了发送、接受事件
- sharedworker
-
- 类似于websocket,提供了发送、接受事件
浏览器 Storage 与 Cookie
| 浏览器 Storage | localStorage | sessionStorage | cookie |
|---|---|---|---|
| 作用 | 在客户端存储值 | ||
| 主要用于在不同页面或会话之间进行数据的持久化和共享 | 主要用于在客户端和服务器之间存储状态信息,以便在不同的 HTTP 请求之间传递数据 | ||
| 存储时间 | 持久存储,除非手动删除或清除缓存 | 临时存储,会话结束数据删除 | 有过期时间不设置过期则属于临时存储,会话结束数据删除 |
| 存储大小 | 5MB | 4KB | |
| 跨标签页共享 | 支持 | 不支持 | 支持 |
| 使用场景 | 需要长期存储的数据,如用户首选项、主题选择等 | 适合用于临时保存在用户会话期间需要共享的数据,如购物车内容、会话令牌等。 | 主要用于在客户端和服务器之间传递数据,例如用户身份验证、会话管理和跟踪用户活动等。 |
| 语法代码 | xxStorage.setItem("x", xx));xxStorage.getItem("x", xx));xxStorage.removeItem("xx");xxStorage.clear(); | document.cookie = "xx" |
Promise 前世今生
什么是 Promise?
是 JS 处理异步的一种方式,用来解决回调地域,将本应该嵌套的东西处理为链式调用
那什么是异步?
那就要从 JS 说起,JS 是单线程的
为啥 JS 是单线程呢?
因为 JS 是可以操作 DOM 的,那如果 JS 是多线程,那同时对同一个 DOM 进行操作时,那如何确认优先级?
所以为了避免该情况,JS 就被设计为单线程,代码只能一行行执行,执行完当前代码才继续执行,如果未执行完就一直等待,就会发生阻塞,如果执行报错,则中断执行。
所以也就有了同步代码与异步代码
那既然是单线程,那如何处理定时器、Ajax 请求等事件呢?
因为定时器、ajax 请求等事件是需要等待,然后又为了避免出现一直等待,导致浏览器阻塞,并且空闲的资源被浪费,就设计了事件循环来解决这种问题。
这种遇到了不立马执行,而是需要等待后再执行的,就被统称为“异步事件”
那啥是事件循环?
事件循环是指:JS 先执行完执行栈中代码,然后通过轮询的方式从任务队列里面拿可执行的任务,并执行。
那是啥任务队列?
任务队列:里面存放的是“异步”事件执行完成后的回调函数,根据其放入的顺序,逐渐形成一个队列
里面又分为:微任务队列、宏任务队列
常见微任务:Promise.then 里面的代码、await 之后的代码等,队列由 JS 引擎线程维护
常见宏任务:
所以 Promise 是 ES6 提供的一种处理异步的方式
Promise 基础
基础使用语法
new Promise((resolve,reject) => {
const a = 1;
resolve(true)
}).then(res => {
console.log(res) // true
})
规范
Promise 的状态有哪些?
pending | fulfilled | rejected
Promise 的默认状态?
pending
状态如何流转?
pending => fulfilled
pending => rejected
Promise的返回值是什么?
是个 then 方法:接收 onFulfilled 和 onRejected
手写 Promise
function newPromise(executor) {
const pending = "pending";
const fulfilled = "fulfilled";
const rejected = "rejected";
// 1. 默认状态-pending
this.state = pending;
// 2. 内部变量
this.value = undefined;
this.reason = undefined;
// 新增两个变量来存储成功和失败的回调函数
this.resolveCallbacks = [];
this.rejectedCallbacks = [];
// 3. 成功的回调
const resolve = (value) => {
// 状态单向流转控制
if (this.state === pending) {
this.state = fulfilled;
this.value = value;
this.resolveCallbacks.forEach((cb) => cb(this.value));
}
};
// 4. 成功的回调
const reject = (reason) => {
// 状态单向流转控制
if (this.state === pending) {
this.state = rejected;
this.reason = reason;
this.rejectedCallbacks.forEach((cb) => cb(this.reason));
}
};
executor(resolve, reject);
this.then = function (onFulfilled, onRejected) {
if (this.state === fulfilled) {
onFulfilled(this.value);
}
if (this.state === rejected) {
onRejected(this.reason);
}
if (this.state === pending) {
this.resolveCallbacks.push(() => onFulfilled(this.value));
this.rejectedCallbacks.push(() => onRejected(this.reason));
}
};
this.catch = function (onRejected) {
if (this.state === rejected) {
onRejected(this.reason);
}
};
}
const p = new newPromise((resolve, reject) => {
console.log(1);
setTimeout(() => {
resolve("xxx");
}, 1000);
});
p.then((res) => {
console.log("[ then ] >", res);
});
补充知识
async/await
基于 Promise 机制,将异步代码的编写变得更同步化。
本质上就是个“语法糖”
面试题
1. 同步、异步基础
console.log(1)
setTimeout(()=>{
console.log(2)
},0)
console.log(3)
function fn(){
console.log(4)
setTimeout(()=>{
console.log(5)
},0)
}
fn()
// 问题:打印结果
// 答案:
// 1
// 3
// 4
// 2
// 5
// 原因分析:
// 先创建了全局执行上下文,里面要执行的代码分为:同步、异步
// 全局执行上下文---start
// 然后一行行执行,所以打印 1
// 然后遇到了 setTimeout,是异步任务,则将其回调函数放到事件队列中
// 继续一行行执行,所以打印 3
// 然后又遇到 函数的声明,这仅仅是声明所以继续往下执行
// 最后遇到了函数的调用,则创建函数执行上下文,并执行函数
// 函数执行上下文---start
// 进入函数内,也是一步步执行,所以先打印 4
// 然后遇到了 setTimeout,是异步任务,则将其回调函数放到事件队列中
// 然后函数内的同步代码就执行完毕了
// 开始进入事件队列中,根据先进先出原则,所以先执行全局里面定义的 setTimeout
// 所以先打印 2
// 之后再执行函数里面定义的 setTimeout,所以打印 5
2. 打印顺序1
setTimeout(() => console.log(1));
new Promise((resolve) => {
console.log(2);
resolve();
})
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7))
console.log(8);
Promise.resolve(true)
.then(() => console.log(9))
.then(() => console.log(10))
.then(() => console.log(11))
.then(() => console.log(12))
// 问:以上代码打印顺序
// 答:
// 2
// 8
// 3
// 9
// 4
// 10
// 5
// 11
// 6
// 12
// 7
// 1
3. 打印顺序2
setTimeout(() => console.log(1), 0);
new Promise((resolve) => {
console.log(2);
setTimeout(() => {
console.log(3);
new Promise((resolve) => {
console.log(4);
setTimeout(() => {
console.log(5);
resolve(true);
}, 0);
}).then(() => console.log(6));
resolve(true);
}, 0);
})
.then(() => console.log(7))
.then(() => console.log(8))
.then(() => {
setTimeout(() => {
console.log(9);
}, 0);
});
console.log(10);
Promise.resolve(true).then(() => console.log(11));
setTimeout(() => {
console.log(12);
Promise.resolve(true)
.then(() => console.log(13))
.then(() => console.log(14))
.then(() => console.log(15));
}, 0);
// 问:以上代码打印顺序
// 答:
// 2
// 10
// 11
// 1
// 3
// 4
// 7
// 8
// 12
// 13
// 14
// 15
// 5
// 6
// 9
4. 打印顺序3
let a = () => {
setTimeout(() => {
console.log('任务队列函数1')
}, 100)
for (let i = 0; i < 5000; i++) {
console.log('a的for循环')
}
new Promise((resolve)=>{
console.log('a事件的Promise')
resolve()
}).then(()=>{
console.log('a事件的Promise.then')
})
console.log('a事件执行完')
}
let b = () => {
setTimeout(() => {
console.log('任务队列函数2')
}, 20)
for (let i = 0; i < 5000; i++) {
console.log('b的for循环')
}
new Promise((resolve)=>{
console.log('b事件的Promise')
setTimeout(()=>{
console.log('b事件的Promise的setTimeout')
new Promise(r=>r()).then(()=>console.log('b事件的Promise的Promise.then'))
resolve()
},0)
}).then(()=>{
console.log('b事件的Promise.then')
})
console.log('b事件执行完')
}
let c = () => {
setTimeout(() => {
console.log('任务队列函数3')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('c的for循环')
}
new Promise((resolve)=>{
console.log('c事件的Promise')
setTimeout(()=>{
console.log('c事件的Promise的setTimeout')
resolve()
},100)
}).then(()=>{
console.log('c事件的Promise.then')
})
console.log('c事件执行完')
}
a();
b();
c();
// 打印结果:
// 5000次 a的for循环
// a事件的Promise
// a事件执行完
// 5000次 b的for循环
// b事件的Promise
// b事件执行完
// 5000次 c的for循环
// c事件的Promise
// c事件执行完
// a事件的Promise.then
// b事件的Promise的setTimeout
// b事件的Promise的Promise.then
// b事件的Promise.then
// 任务队列函数3
// 任务队列函数2
// 任务队列函数1
// c事件的Promise的setTimeout
// c事件的Promise.then
排版更好:《1-3 Promise 知识与使用》
参考:浏览器工作原理 - 掘金