1.本文的目的
文章从JS事件环工作原理进行分析, 并且结合代码案能清晰的了解到我们常说的宏任务、微任务、UI渲染以及JS遇到异步时候都干了些什么等等,并且解释了为什么微任务会在宏任务前进行执行等一系列问题。希望对大家理解JS执行有帮助!
2. 浏览器是多进程的
这里简单的提一下进程与线程的概念, 以及为什么浏览器是多进程的。
a. 什么是进程
- CPU正在进行的一个任务的运行过程的调度单位
- 进程是计算机调度的基本单位
- 进程包含线程, 线程在进程中运行
b. 浏览器为什么是多进程的?
注意现象: 我们可以通过任务管理器来查看, 当我们打开谷歌浏览器的时候可以在进程栏目看到很多的Chrome的进程, 每当谷歌浏览器打开一个新的tab页会发现任务管理器里面的谷歌进程会越来越多.
因为谷歌浏览器为每一个tab页都是一个独立的进程. 这样做的好处是其中一个崩溃了(例如程序出现了死循环)不会影响其他的tab页正常的访问,它们之间可以相互独立不干扰。
c. 浏览器有哪些主要的进程
- 主进程 - 浏览器的用户界面
- 每一个tab有各自独立的渲染进程(渲染引擎)、 网络进程(网络请求)、GPU进程(动画绘制)、插件进程(vue的开发插件等)
3. 特别重要的渲染进程
在了解了浏览器的一些基本的进程功能后要重点理解一些渲染进程, 因为这和我们写的页面代码息息相关。
a. 渲染进程主要包含的两个线程以及功能
- GUI渲染线程(主要负责渲染页面)
- 解析HTML、CSS
- 构建DOM/Render树
- 初始布局与绘制
- 重绘和回流
- JS引擎线程
-
解析JS脚本
-
运行JS代码
-
b. GUI渲染线程与JS引擎线程运行互斥(重点)
从下面两个案例可以看出渲染线程与JS引擎关系, 以及为什么它们必须是互斥关系
两者之间的关系: 运行的时候相互互斥, 当JS引擎任务空闲的时候GUI渲染才会更新。
// 案例1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>one</div>
<script>
while (true) {};
</script>
<div>two</div>
</body>
</html>
// 案例2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="app">one</div>
<script>
setTimeout(() => {
document.getElementsByClassName('app')[0].innerText = 'three'
}, 1000);
</script>
<div>two</div>
</body>
</html>
案例1中是一个死循环, 这里有一个问题为什么没有渲染one然后才卡主, 而是一进入页面什么都没有就已经卡主了,这个问题当后面了解了事件环相关的知识后就能轻松明白背后的逻辑。 案例2设置了一个延时器使得修改文字函数成了异步任务, 第一次执行的时候JS是空闲的所以渲染了one two。但是当第二次异步任务完成的时候也会将one改成了three, 这里后面也会解析相关的步骤。
GUI与JS引擎线程互斥的原因: 因为JS能够操作并且改变dom结构, 渲染的过程中如果JS还在改变dom, 那么就会产生冲突 所以JS在运行的GUI要冻结挂起。
c. 渲染进程包含的事件线程
渲染引擎还有和事件相关的线程, 事件环Event Loop线程就在其中。
- 事件触发线程: 事件环 Event Loop线程
- 事件线程: 用户交互事件、setTimeout 、Ajax
4. 宏任务与微任务
很多地方都讲了宏任务和微任务, 但是个人感觉讲的都比较复杂,特别是对新手理解起来还是有一定的难度。这里简单明了的说一下宏任务和微任务的区别和目的。
宏微任务简单的区分 其实很简单宏任务就是宿主浏览器提供的异步方法和任务例如 script脚本、setTimeout、UI渲染。微任务就是JS语言提供的API,例如Promise MutationObserver;
为什么要区分宏任务和微任务? 其实目的就如同和创建两个线程一样, 能独立完成各自的任务互相不干扰。宏任务是由浏览器提供的而微任务是由JS语言提供的,并且微任务优先级更高。这样它们执行不会相互干扰(后面案例有详细说明);
宏任务与微任务在任务队列中两者不同之处: 宏任务每次取一个出来先进先出的原则, 而微任务在执行队列中一次性执行完毕并且清空。(这里可以配合后面的案例能更好的理解, 先记住这个特点)。
5. 案例详解
分析这个案例可以很清楚的了解到JS的执行机制以及事件环的处理逻辑
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
document.body.style.backgroundColor = 'blue';
console.log(1);
setTimeout(() => {
document.body.style.backgroundColor = 'red';
console.log(2);
}, 100);
Promise.resolve(3).then(number => {
document.body.style.backgroundColor = 'orange';
console.log(number);
})
console.log(4);
</script>
</body>
</html>
这是一个常见的面试题,主要是考异步关系和宏微任务。相信很多人都能答出输出顺序来, 但是如果问题是哪些颜色GUI没有渲染或者这个代码在事件环的执行机制是怎样的呢?下面我们深度的分析一下这段代码在JS中的执行步骤吧。
先了解事件环的特点
看图
执行栈概念: 程序放到里面一个一个的执行;
执行特点: 每次循环从JS引擎线程开始依次往后; 从图中我们可以看出第一次循环先执行JS, JS执行完毕后会进入微任务队列进行执行,然后才会进行GUI渲染,这也是第一个无限循环案例中没有渲染节点one而直接卡死的原因;
步骤分析:
- 先走同步任务
document.body.style.backgroundColor = 'blue';
console.log(1);
- 遇到宏任务-延时器任务
setTimeout(() => { document.body.style.backgroundColor = 'red'; console.log(2); }, 100);
将延时器任务丢入到宏任务队列中
- 遇到微任务-Promise.then()
Promise.resolve(3).then(number => {
document.body.style.backgroundColor = 'orange';
console.log(number);
})
将这个任务放入到了为任务队列。注意这里有一个概念:Promise是微任务, 但不一定会进入队列 ,进入队列一定是触发了then方法!Promise.resolve(3) 这句代码就不会进入微任务队列,因为它是同步执行的。关于promise的细节问题之后可以专门写一篇文字进行具体分析。
- 进入微任务队列 console.log(4)同步任务放入执行栈进行执行, 当执行完了console.log(4)后执行栈里所有的代码都执行完毕了, 所以进入到了微任务队列中。由于微任务队列中每次都需要清空所以将里面的代码全部都执行了也就是then的回调。
- 进入GUI渲染 执行栈中有背景颜色改变的指令以及微任务队列中也有背景颜色改变的指令,此时GUI渲染队列中有两条渲染指令。 由于都是背景颜色修改所以执行后面一条颜色修改指令。所以当进入页面的时候最开始只有orange颜色而不会显示blue颜色, 因为第一次轮询到渲染的时候,微任务的颜色改变指令覆盖掉了同步任务中的颜色修改指令。具体可看图
-
到宏任务队列中发现宏任务队列的延时器并没有达到执行条件(100ms)所以不会将延时器的回调函数放入到JS执行栈中执行;
-
继续下一次轮询工作 JS执行栈执行完毕 -> 微任务队列执行并清空 -> gui渲染 -> 宏任务队列, 当宏任务队列中的延时器满足了触发条件会放入到JS执行栈中,也就是延时器里面的回调函数。
- JS栈执行完后由于微任务中没有任务所以直接到GUI渲染将背景颜色改为red;
根据上述步骤我们可以分析出输出的结果为 1 4 3 2,并且背景颜色开始是orange然后变为了red;刚好也解释了前面提出的问题;
5. 总结
结合案例我们能很清楚的了解事件环的执行逻辑、宏任务和微任务的区别在哪以及JS对异步任务的处理,但是件环还有更多的细节有待深挖,例如如果宏任务队列中又有微任务又会怎么处理?这只是一个简单的案例,以后有时间会根据更复杂的案例来分析。希望能帮助大家。Thanks♪(・ω・)ノ