event loop
浏览器内核是多线程,
Javascript
是单线程。
楼主之前讲解了js
的异步处理,只是讲解了异步处理的方法,但是对于异步处理的原理还是不是很了解,这篇文章是对于浏览器的线程方面对JavaScript
的运行机制进行分析。
1:浏览器基本
我们经常说,js
的执行环境是一个单线程,会按顺序执行代码,但是JavaScript
又是可以异步,这两者感觉有冲突,但是本质上,如果理解浏览器的事件循环机制(event loop),就会觉得并不冲突。
浏览器里面不仅只有解释JavaScript
的引擎,还包括很多其它的引擎。
浏览器主要包括:
- 用户界面:包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
- 浏览器引擎(浏览器内核): 在用户界面和呈现引擎之间传送指令。
- 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
- 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
- JavaScript 解释器。用于解析和执行 JavaScript 代码。
- 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
我们今天主要讨论的是浏览器引擎(浏览器内核)和 JavaScript 解释器(V8引擎)之间的交互和沟通。
2:浏览器引擎(浏览器内核)
浏览器内核的是一个多线程处理,它主要包含如下几个线程
- GUI渲染线程: 渲染页面的
html
元素 - JavaScript引擎线程: 页面的交互和
dom
渲染 - 定时触发器线程:一定时间后,来触发对应的线程
- 事件触发线程:当一个事件触发该线程的时候,就会把它放到js的事件队列中等待执行。常用于异步操作。
- 异步http线程: 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。
他们之间有一下的联系:
- JavaScript引擎和GUI引擎互斥,不能一边操作
dom
一边渲染页面 - JavaScript引擎是单线程,所有需要按照事件处理队列来处理相应的代码。
- JavaScript引擎有一个监听事件(monitoring process)的功能,会持续不断的检查
js
引擎的主线程执行栈是否为空,如果为空就会去取事件触发线程存放在事件队列中的回调函数执行。
3:js引擎执行机制
由于js的运行环境是单线程的,一些异步操作还是需要借助于浏览器这个宿主来实现。这里简单的一个图来描述js
运行的时候的流程。主要运用了浏览器的js引擎线程和事件触发线程,有时候开启网络服务和定时器也会用到其他的线程。
4: micro task 和 macro task
浏览器的事件循环依靠已事件队列,但是一个进程中不止一个事件队列,大致可以分为micro task
和macro task
,常见的微任务和宏任务分别包括:
micro task :
- nextTick
- callback
- Promise
- process.nextTick
- Object.observe
- MutationObserver
macro task:
- setTimeout
- setInterval
- I/O
- script代码块
主要部分: 事件队列在同步队列执行完后,首先会执行nextTick,等nextTick执行完成后,然后会先执行micro task, 等micro task队列空了之后,才会去执行macro task,如果中间添加了micro task加入了micro task队列,会继续去执行micro task队列,然后再回到macro task队列。js引擎存在monitoring process进程, 会不停的监听task queue
它们的细节可以参考Tasks和Microtasks
5:event loop和宏任务、微任务
一段代码块就是一个宏任务。所有一般执行代码块的时候,也就是程序执行进入主线程了,主线程会根据不同的代码再分微任务和宏任务等待主线程执行完成后,不停地循环执行。
主线程(宏任务) => 微任务 => 宏任务 => 主线程
下图是一个简易的时间循环:
6: 简易的event loop
console.log('start')
Promise.resolve().then((resolve) => {
console.log(1)
}))
console.log('end')
大致流程:
- 这一个代码块相当于一个macro-task,进入主线程。
- 遇到同步的代码
console.log('start')
开始执行 -
Promise
是micro-task,把它的回调函数放入微任务Event Queue中 - 遇到
console.log
开始执行,执行完后,这个代码块宏任务完成,js
监听进程发现主线程空了,就会去寻找微任务。 - 去微任务Event Queue找到Primise的回调函数,执行。
图解浏览器内部执行:
7:复杂的分析
这里我们通过2个复杂的代码来检验是否已经基本了解了事件循环的机制:
// 来至于谷友的一到面试题
<script>
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
</script>
整个代码块作为一个宏任务,进入主线程
看到函数申明但没有执行,遇到函数console.log执行,输出
script start
-
遇到
setTimeout()
,把它的回调函数放入宏任务(setTimeout1)。宏任务 微任务 setTimeout1 遇到执行
async1()
, 进入async
的执行上下文之后,遇到console.log
输出async1 start
然后遇到
await async2()
,由于()
的优先级高,所有先执行async2()
,进入async2()
的执行上下文。-
看到
console.log
输出async2
,之后没有返回值,结束函数,返回undefined
,返回async1
的执行上下文的await undefined
,由于async
函数使用await
后得语句会被放入一个回调函数中,所以把下面的放入微任务中。宏任务 微任务 setTimeout1 async1=> awati 后面的语句 -
结束
async1
,返回全局上下文,遇到Promise
构造函数,里面的函数立马执行, 输出promise1
, 之后的回调函数进入微任务宏任务 微任务 setTimeout1 async1=> awati 后面的语句 new Promise() => 后的then 执行完Promise(),遇到console.log,输出
script end
,这里一个宏任务代码块执行完毕。在主线程执行的过程中,事件触发线程会一直监听异步事件,当异步事件处理完成后,把它的回调函数放入事件队列,等待执行。
主线程现在空闲下来后,执行事件队列中的微任务,然后继续向下执行,遇到
new Promise()
后面的回调函数,执行代码,输出promise2
(这里2个微任务的优先级,promise高于async)。-
看到
async1
中await
后面的回调函数,执行代码,输出async1 end
宏任务 微任务 setTimeout1 空 此时微任务中的队列为空,开始执行队列中的宏任务,进入一个新的代码块。遇到
console.log
,输出setTimeout
执行完成,最后结果为
```javascript
script start => async1 start => async2 => promise1 => script end => promise2 => async1 end => setTimeout
```
8:总结
- JavaScript运行环境是单线程,不管什么代码,什么框架的异步代码,都是利用宿主对象(Node,浏览器)的其他线程来通过事件循环机制实现
- 我们一般说的运行和执行是不一样的,JavaScript的运行环境是JavaScript解析引擎,执行环境比如node,浏览器。
- JavaScript解析引擎会监听事件循环机制,把异步任务放入事件队列。
链接参考: