说一说JS的事件循环
# 为什么会有事件循环机制
JavaScript的一大特点就是单线程,也就是说,同一时间只能做一件事。那为什么要设计成单线程呢,多线程效率不是更高吗?
有这样一个场景:假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,JavaScript从诞生就是单线程。但是单线程就导致有很多任务需要排队,只有一个任务执行完才能执行后一个任务。如果某个执行时间太长,就容易造成阻塞;为了解决这一问题,JavaScript引入了 事件循环机制
# 事件循环是什么
Javascript单线程任务被分为同步任务和异步任务。
同步任务:立即执行的任务,在主线程上排队执行,前一个任务执行完毕,才能执行后一个任务;
异步任务:异步执行的任务,不进入主线程, 而是在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候读取执行。
注意:
异步函数在相应辅助线程中处理完成后,即异步函数达到触发条件了,就把回调函数推入任务队列中,而不是说注册一个异步任务就会被放在这个任务队列中
同步任务与异步任务流程图:
从上面流程图中可以看到,主线程不断从任务队列中读取事件,这个过程是循环不断的,这种运行机制就叫做Event Loop(事件循环)!
# 事件循环中的两种任务
在JavaScript中,除了广义的同步任务和异步任务,还可以细分,一种是宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)。
二者执行顺序流程图如下:
每次单个宏任务执行完毕后, 检查微任务队列是否为空, 如果不为空,会按照先入先出的规则全部执行完微任务后, 清空微任务队列, 然后再执行下一个宏任务,如此循环
如何区分宏任务与微任务呢?
- 宏任务:macrotask,又称为task, 可以理解为每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
一般包括:
script(整体代码)
setTimeout()
setInterval()
postMessage
I/O
UI交互事件
微任务:microtask, 又称为job, 可以理解是在当前 task 执行结束后立即执行的任务。
包括:
Promise.then/cath /finally回调(平时常见的)
MutationObserver回调(html5新特性)
# 为什么要引入微任务
为什么要引入微任务,只有一种类型的任务不行么?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。
因为事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取, 那如果突然来了一个优先级更高的任务,还让去人家排队,就很不理性化, 所以需要引入微任务。
注意:
在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
# 参考文章链接
从一道面试题谈谈对EventLoop的理解 (opens new window)
这次彻底搞懂浏览器的事件循环知识它、!(面试必备) (opens new window)
高频面试题:JavaScript事件循环机制解析 (opens new window)
JavaScript 运行机制详解:再谈Event Loop (opens new window)
JavaScript 事件循环(EventLoop) —— 浏览器 & Node (opens new window)