JS事件循环
众所周知,JS是一门单线程语言,但是它却又很多异步特性。这是因为它有自己的并发模型,基于任务队列和事件循环。
任务队列
当用户触发一个事件,如果该事件有处理器handler
,那么该事件就被当成一个任务推入任务队列里。
为什么不是直接执行该事件处理器呢?
因为JS单线程的原因,同一个时间点只能执行一段代码。一个frame
中的UI
,DOM
,用户输入
都是由一个线程完成。那么一旦某个事件处理器的代码逻辑执行时间过长,那么体现出来的就是页面卡顿无响应。
使用任务队列就可以缓解这个问题,同一时间可以有多个事件被触发,多个用户输入,这些都被作为任务记录下来,推入到任务队列中,并且暂时并不去立即执行。
这些任务如何进行处理,处理逻辑是什么?就牵扯到了事件循环。
事件循环
JS引擎中有栈,堆,和任务队列。其中栈负责函数的调用,堆存储对象,而任务队列则负责异步事件的处理。
通常一个函数A
被调用,就会被push
到栈中,如果这个函数还调用了B
,那么B
也会被push
到栈中。直到函数执行完毕,才会被弹出栈。
当栈为空时,表示前一个函数逻辑已经执行完毕,等待下一步操作。这个时候,任务队列就会将队列中的任务(事件处理器)推入栈中执行。然后再等待栈空,继续推入下一个任务(循环)。直到任务队列也为空。
每一个任务完整地执行后,其它任务才会被执行。
JS中的所有异步操作/事件处理器都是将回调函数推入任务队列中,其具体执行时间得看它前面的任务执行时长。
setTimeout
说起任务队列就得不得不提的setTimeout(fn, timeout)
,其中的timeout
是指至少在多少毫秒将回调函数fn
推入任务队列。
也就是说fn
进入任务队列的时长是大于等于timeout
的,其真正的执行时间间隔,也是大于timeout
的。
永不阻塞
由于任务队列的性质,JS将每个操作都存储了下来,所以它不会阻塞(除非你的代码)。
宏任务和微任务
一个事件循环中,会有一个或多个任务队列和一个微任务队列。有了一个任务队列,为什么还要一个微任务队列呢?
首先,在宏任务之间,是会存在渲染的。但是微任务不会,微任务属于一个宏任务。
那么哪些操作会发起一个微任务呢?promise
,mutation observer
的回调都会添加一个微任务。当当前调用栈为空时,微任务队列就会出列并且执行,直到微任务队列为空并且调用栈为空,才会开始下一个宏任务。