1. 前言
很多网上的教程都想教会大家浏览器的渲染原理,我想这对于初学者来说可能还是过于复杂了,今天我想用简单的几个例子方式让大家理解为什么 js
中有那么多异步操作, js
却还是单线程的。
2. 阻塞的页面
首先我们来看看下面这个例子
<!DOCTYPE html>
<html lang="en">
<head>
<title>demo</title>
</head>
<body>
<div>hello world</div>
</body>
<script>
while (true) {
}
</script>
</html>
运行这个例子后,可以发现页面上始终不会渲染 hello world
。
这是因为运行 js
任务的被设计为了单线程的,当一个任务一直不结束时,后面的任务永远也不会执行,而浏览器的渲染任务不幸的和 js
任务处在同一个队列中。
所以 js
网页有一个问题就是常常不能处理复杂任务,因为这会阻塞住后续任务的执行,这也就是为什么经常写一个稍微复杂一点的组件就需要考虑性能问题最大的原因。
3. 同样都是编程,为啥我不是多线程呢
首先需要明确的是浏览器并不是单线程的,甚至每一个页面都可以是一个进程合并多个线程。那么为什么单单是 js
运行时必须要是单线程的呢?
我们可以来模拟一下这样的场景
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>草莓的价格是: <div id="price"></div></div>
<div>输入框1:<input id="priceInput1" type="" name=""></div>
<div>输入框2:<input disabled id="priceInput2" type="" name=""></div>
<button id="priceButton">修改</button>
</body>
<script type="text/javascript">
</script>
</html>
多输入几次执行后,可以发现草莓的价格有时会是第一个输入框的值,有时会是第二个输入框的值。
可以想象到如果我们的当前 js
执行和用户操作触发执行的js
是两个线程的话, 因为同一个时刻的两个线程会互相竞争,那数据最终的值就变得不可确定了。
这样的话可能需要花费大量时间来处理这种多个线程同时修改一个数据的情况。
也就是如果想的话 js
也可以是多线程运行的,但是为了避免多线程造成的冲突问题,浏览器主动选择了单线程的这种模式来执行任务
4. 单线程的实现机制
那为啥我在使用 js
时常常也有异步的操作,这些难道不是多线程执行的吗?
这些可以理解为多线程的情况,但是需要注意的是浏览器设置了一个 浏览器的执行队列,让你的 js
变成同步执行了, 浏览器的执行队列 中的任务会依次由单个线程执行完前一个任务才会处理执行后一个任务。
也就是虽然你的比如 ajax、用户事件、定时器 这些在浏览器底层是异步的,但是最终回调的函数放到浏览器的执行队列中又是同步执行的了
可以看到所有被触发的任务都会先放入 宏任务
和 微任务
的队列中,然后再被放入到 浏览器的执行队列 中等待所有前面的任务都被执行完后再被执行。
那么宏任务和微任务的区别是什么呢?其实就是 微任务
队列的优先级更高而已,浏览器会先将 微任务
队列里所有的任务都执行完毕 ,才会处理 宏任务
队列的任务。
另外就是浏览器会在任务执行的间隙插入渲染任务来保证页面的正常渲染。也就是如果你浏览页面过程中发现页面有卡顿现象,需要优先考虑是否有任务阻塞住了浏览器的执行队列。
或者本身渲染任务过于庞大,比如需要渲染大量文本、图片节点等等,导致渲染任务执行时间过长导致页面卡顿。
当然浏览器任务的执行规则没有这么简单,这里将整个流程进行了简化,方便理解
5. 一个简单的面试题
在初学前端的时候我就遇到这样的一个面试题: 就是 setTimeout(() => {}, 0) 这段代码会立即执行吗? 当初我没有回答出来,我想看完这篇文章的你,应该能很轻松的回答这个问题了。