01.JS执行机制
浏览器常驻线程
- 我们常说JS是单线程,但是浏览器中包含了很多线程
-- js引擎线程(解释执行js代码,用户输入,网络请求)
-- GUI线程(绘制用户界面,与js主线程是互斥的)
-- http网络请求线程(处理用户的get、post等请求,等返回结果后将回调函数推入任务队列)
-- 定时触发器线程(setTimeout、setInterval等带时间结束后爸执行函数推入任务队列中)
-- 浏览器事件处理线程(将click、mouse等交互事件发生后将这些事件放入事件队列中)
JS引擎线程和GUI线程是互斥的
JS可以操作DOM元素,进而会影响到GUI的渲染结果,因此JS引擎线程与GUI渲染线程是互斥的。也就是说当JS引擎线程处于运行状态是,GUI渲染线程将处于冻结状态
比如有下面这一段代码:
<button onclick="fn1()">click me</button>
<script>
dieloop()
function dieloop(){
while(true){
}
}
function fn1(){
console.log(1)
}
</script>
执行以后浏览器不断加载,但是不会显示出按钮,原因就是JS引擎线程不断在处理死循环中的代码,而此时GUI渲染线程处于冻结状态,所以页面上不会渲染出按钮。
JS执行机制——单线程、非阻塞
javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。
- 单线程-同一时间只能做一件事
单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。
而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
多线程不好吗?至少对于JS这门语言设计初衷来说是不好的,JS设计出来就是为了与用户交互,处理DOM,加入js是多线程,同一时间一个线程想要修改DOM,另一个线程想要删除DOM,问题就会变得复杂许多,浏览器不知道听谁的,如果引入“锁”的机制,这不就又回到了其他语言尴尬的困境了吗?
大量数据操作怎么办?
单线程计算能力有限,大量数据需要计算渲染的话,我们可以配合后端进行操作,也就是传说中的SSR技术
JS执行机制
JavaScript是基于单线程运行的,同时又是可以异步执行的,一般来说这种既是单线程又是异步的语言都是基于事件驱动的,恰好浏览器就给JavaScript提供了这么一个环境。

同步和异步任务分别进入不同的执行“场所”,同步的进入主线程,异步的进入Event Table并注册函数,当指定的事情完成时,Event Table会将这个函数移入Event Queue。主线程内的任务执行完毕为空,会去EventQueue读取对应的函数,进入主线程执行。上述过程会不断重复,也就是常说的Event Loop(时间循环)
- 下面看一段代码:
<script>
function foo(ot){
function bar(it){
console.log(it)
}
bar(20)
console.log(ot)
}
foo(10)
</script>
- 这段代码描述了同步任务的执行过程
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。
一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。
- 代码没有执行的时候,执行栈为空栈
- foo函数执行时,创建了一帧,这帧中包含了形参、局部变量(预编译过程),然后把这一帧压入栈(先进后出数据结构)中
- 然后执行foo函数内部代码,执行bar函数
- 创建新帧,同样有形参、局部变量、压入栈中
- bar函数执行完毕,弹出栈
- foo执行完毕,弹出栈
- 执行栈为空
【执行栈其实相当与JS主线程】
- 这段代码代表了异步任务
$.ajax({
url:'localhost:/js/demo.json',
data:{},
success:function(data){
console.log(data)
}
})
console.log('run')
- Ajax进入Event Table,注册回调函数success
- 执行console.log('run')
- ajax事件完成http网络请求线程后把任务放入Event Queue中
- 主线程(调用栈)读取任务执行success函数
重新理解定时器
- setTimeout的等待时间结束以后并不是直接执行而是先推入浏览器的一个任务队列,在同步队列结束后再依次调用任务队列中的任务。
- setTimeout(function(){},0)Js主线程中的执行栈为空时,0毫秒实际上也达不到,根据HTML标准,最低4毫秒。
- setInterval是每个一段时间把任务放到Event Queue之中
js实现异步的具体解决方案
同步代码直接执行
异步函数到了指定时间再放到异步队列
同步执行完毕,异步队列轮询执行。
什么叫轮询?
精简版:当第一个异步函数执行完之后,再到异步队列监视。一直不断循环往复,所以叫事件轮询。
详细版:js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
事实上,事件轮询与宏任务和微任务密切相关。