今天我们来了解一下浏览器的事件环机制。
浏览器模型
- User Interface(用户界面)-包括地址栏、前进/后退按钮、书签菜单等
- Browser engine(浏览器引擎)-在用户界面和呈现引擎之间传送指令
- Rendering engine(呈现引擎)-又称渲染引擎,也被称为浏览器内核,在线程方面又称为UI线程
- Networking(网络)-用于网络调用,比如 HTTP 请求
- UI Backend(用户界面后端)-用于绘制基本的窗口小部件(UI线程)
- JavaScript解释器-用于解析和执行 JavaScript 代码(JS线程)
- Data Persistence(数据存储)-这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie
注意:UI线程和JS线程是互斥的,因为它们两个共用一个线程,即主线程。
JS是单线程的
为什么JS要设计成单线程的?这是由Javascript这门脚本语言的用途决定的。JS的主要工作是操作DOM,如果设计成多线程的,两个线程对同一个元素进行操作,浏览器就不知道该如何处理了。单线程的好处是,我操作的时候你等着,等我操作完成后,你再进行操作,避免冲突。
Philip Roberts的演讲图片
这张图相信大家在很多文章中都见过。
对于上图我找到一个更容易理解的,如下图:
对图片的解释
主线程运行的时候,产生堆(heap)和栈(stack)。在对一个调用栈中的代码进行操作的时候,其他的都要等着。在操作过程中遇到一些类似于setTimeout等异步操作的时候,会交给浏览器的其他模块进行处理。在这些异步操作达到特定条件(定时器等待指定时间之后,ajax请求返回数据)时,把相应的回调函数放入指定的“任务队列”。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
任务队列
大家可以看到图片中写的:宏任务队列和微任务队列。如果大家想详细了解可以看下规范。其中微任务要早于宏任务执行。
常见任务队列
- 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering, MessageChannel
- 微任务:Promise, Object.observer, MutationObserver.
测试代码
在html页面写如下代码:
<script>
console.log(1);
setTimeout(function () {
console.log("4");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log(2);
</script>
<script src="out.js"></script>
<script>
console.log("a");
Promise.resolve().then(() => {
console.log("c");
});
setTimeout(function () {
console.log("d");
}, 0);
console.log("b");
</script>
<script>
console.log("end");
</script>
复制代码
其中 out.js代码如下:
console.log("out1");
setTimeout(function () {
console.log("out4");
}, 0);
Promise.resolve().then(() => {
console.log("out3");
});
console.log("out2");
复制代码
输出结果:
- 1
- 2
- 3
- out1
- out2
- out3
- a
- b
- c
- end
- 4
- out4
- d
根据代码得出结论
前提:script(全局任务)是宏任务 分析:当主线程遇到上面代码时,会把所有的script标签以及外部的js文件放入宏任务队列中(先后顺序就是书写的顺序)。此时主任务队列中没有可执行的代码。所以就取宏任务队列中的第一个宏任务
console.log(1);
setTimeout(function () {
console.log("4");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log(2);
复制代码
先输出 1;遇到setTimeout交给其它模块执行,在到达指定时间(10毫秒或16毫秒)之后会把回调函数放到宏任务队列最后;遇到Promise同样交给其它模块执行,达到条件之后放到微任务队列;再输出 2。此时宏任务队列该执行 out.js文件了,但是微任务队列中已经有微任务在排队了(Promise.resolve().then()中的回调函数)。微任务要早于宏任务执行,所以要先输出 3。再去执行out.js中的代码。out.js中的代码以及后面script标签内的代码和上面代码类似,就不再一一赘述。所以当输出 c 之后。栈(stack)和微任务队列中已没有可以执行的代码。剩下的是宏任务队列中的代码:依次是
<script>
console.log("end");
</script>
function () {
console.log("4");
}
function () {
console.log("out4");
}
function () {
console.log("d");
}
复制代码
然后依次放到主栈中执行,输出:end 4 out4 d 其它宏任务和微任务都遵循这个规则,就不一一举例了。
最后做一下总结:
-
浏览器中的宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering, MessageChannel
-
浏览器中的微任务:Promise, Object.observer, MutationObserver.
-
浏览器的事件环机制:
- 1.所有同步任务都在主线程上执行,形成一个执行栈;主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件;
- 2.执行栈执行过程中,遇到异步操作就交给其他模块处理,只要异步任务有了运行结果,就在任务队列之中放置一个事件(宏任务放到宏任务队列,微任务放到微任务队列);
- 3.一旦执行栈中的所有同步任务执行完毕,系统就会依次读取微任务队列中的全部微任务放到主栈中执行(执行微任务的时候执行第2步);
- 4.清空微任务队列之后,读取一个宏任务,放到主栈中执行(执行宏任务的时候执行第2步)。执行完毕后再去清空微任务队列中的微任务。。。
- 5.主线程不断重复上面的第3、4步。