单线程的 Javascript 为什么可以异步
前言#
看下面一段代码:
console.log("start");
setTimeout(() => {
console.log("children2");
Promise.resolve().then(() => {
console.log("children3");
});
}, 0);
new Promise((resolve, reject) => {
console.log("children4");
setTimeout(() => {
console.log("children5");
resolve("children6");
}, 0);
}).then(result => {
console.log("children7");
setTimeout(() => {
console.log(result);
}, 0);
});
打印结果:
start
chidlren4
chidlren2
chidlren3
chidlren5
chidlren7
chidlren6
疑问:既然 Javascript 是单线程的,为什么不是从上到下的打印结果呢?
浏览器内核是多线程的#
虽然 Javascript 是单线程的,但是浏览器却不是,在内核控制下,各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
- 浏览器
- 浏览器进程
- 渲染进程
Javascript 引擎线程
GUI 渲染线程
定时触发器线程
事件触发线程
异步 HTTP 请求线程
- GPU 进程
- 网络进程
- 插件进程
Javascript 引擎线程
称为主线程,其他可以称为辅助线程,这些辅助线程便是 Javascript 实现异步的关键
Javascript 引擎线程#
Javascript 引擎
,也叫 Javascript 内核,主要负责处理 Javascript 脚本程序,解析 Javascript 脚本,运行代码,例如 V8 引擎
GUI 渲染线程#
GUI 渲染线程
,负责渲染浏览器界面,解析 HTML、CSS,构建 DOM 树和 RenderObject 树,布局和绘制等
当界面需要重绘 (Repaint) 或由于某种操作引发回流 (reflow) 时,该线程就会执行
在 Javascript 引擎线程
运行脚本期间,GUI 渲染线程
都是处于挂起状态的,也就是说被 “冻结” 了
GUI 渲染线程
与Javascript 引擎线程
互斥由于 Javascript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即
Javascript 引擎线程
和GUI 渲染线程
同时运行),那么 GUI 渲染前后获得的元素数据就可能不一致了;因此,为了防止渲染出现不可预期的结果,浏览器设置GUI 渲染线程
与Javascript 引擎线程
为互斥的关系,当Javascript 引擎线程
执行时,GUI 渲染线程
会被挂起,GUI 更新会被保存在一个队列中,等到Javascript 引擎线程
空闲时立即被执行。
Javascript 阻塞页面加载
由于
GUI 渲染线程
与Javascript 引擎线程
是互斥的关系,当浏览器在执行 Javascript 程序的时候,GUI 渲染线程
会被保存在一个队列中,直到 Javascript 程序执行完成,才会接着执行;因此,如果 Javascript 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
定时触发器线程#
setInterval
和 setTimeout
所在线程,避免 Javascript 引擎线程
处于阻塞线程状态影响计时的准确
事件触发线程#
将满足触发条件的事件放入任务队列
当一个事件被触发时,事件触发线程
会把事件添加到待处理队列的队尾,等待 Javascript 引擎线程
的处理;这些事件可以是当前执行的代码块,如定时任务,也可来自浏览器内核的其他线程,如鼠标点击、Ajax 异步请求等,但由于 Javascript 的单线程关系,所有这些事件都得排队,等待 Javascript 引擎线程
处理。
异步 HTTP 请求线程#
XMLHttpRequest 在连接后,通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 Javascript 引擎线程
的处理队列中等待处理。
任务队列#
任务队列,即事件循环 (Event Loop),Javascript 管理事件执行的一个流程
当任务加入到任务队列
后并不会立即执行,而是处于等候状态,等主线程处理完了自己的事情后,才来执行任务队列
中任务
任务又分为两种:
宏任务
(MacroTask 或者 Task)- script
- setInterval/setTimeout
- setImmediate(NodeJS)
- requestAnimationFrame
- I/O
- ajax
- 事件绑定
- MessageChannel
- postMessage
微任务
(MicroTask)- UI rendering
- Promise
- process.nextTick(NodeJS)
- Object.observe
- MutationObserve
微任务
拥有更高的优先级,可以插队,当事件循环遍历队列时,先检查微任务
队列,如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务
需要注意的是:
- 一个事件循环可以有一个或多个事件队列,但是只有一个
微任务
队列。 微任务
队列全部执行完会重新渲染一次- 每个
宏任务
执行完都会重新渲染一次 - requestAnimationFrame 处于渲染阶段,不在
微任务
队列,也不在宏任务
队列
Web Worker#
Web Worker
能够同时执行两段 Javascript,不代表 Javascript 实现了多线程,Web Worker
是向浏览器申请一个子线程,该子线程服务于主线程,完全受主线程控制
- 同源限制
- DOM 限制
- 通信限制
- 脚本限制
- 文件限制
同源限制#
分配给 Worker 线程运行的脚本文件,必须和主线程的脚本文件同源
DOM 限制#
Worker 线程所在的全局对象与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用 document、window、parent 这些对象,但是,Worker 线程可以使用 navigator 对象和 location 对象
通信限制#
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成
脚本限制#
Worker 线程不能执行 alter 和 confirm 方法,但可以使用 XMLHttpRequest 对象发出 Ajax 请求
文件限制#
Worker 线程无法读取本地文件,即不能打开本机的文件系统 (file://),它所加载的脚本必须来自网络
最后#
回到前言,分析执行流程:
- 先执行
宏任务
script 脚本 - 执行
console.log("start")
- 遇到定时器,交由
定时触发器线程
,等待时间到了加入队列 - 遇到 Promise 直接执行 executor,打印
console.log("children4")
;遇到第二定时器,又交由定时触发器线程
管理,等待加入队列;Promise.then
等 resolve 之后加入微队列;此时,第一轮任务执行完毕 - 第一定时器先进入队列,取出任务执行
console.log("children2")
,此时遇到 Promise 执行,并将Promise.then
放入当前宏任务队列中的微任务队列,当前任务执行完毕;执行 then,打印console.log("children3")
- 取出第二定时器,打印
console.log("children5")
,并将 then 放入微任务中,当前宏任务执行完毕,取出 then 执行打印console.log("children7")
- 又遇到定时器,由
定时触发器线程
等待时间到了添加到宏任务中 - 取出定时器任务,打印
console.log("children6")
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: