Event Loop(事件循环)
JavaScript是单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。
任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
NOTE
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
同步与异步执行顺序
- JavaScript将任务分为同步任务和异步任务,同步任务进入主线中中,异步任务首先到Event Table进行回调函数注册。
- 当异步任务的触发条件满足,将回调函数从Event Table压入Event Queue中。
- 主线程里面的同步任务执行完毕,系统会去Event Queue中读取异步的回调函数。
只要主线程空了,就会去Event Queue读取回调函数,这个过程被称为Event Loop。
常见异步任务
- DOM事件
- AJAX请求
- 定时器setTimeout和setlnterval
- ES6的Promise
宏任务和微任务
JavaScript除了广义上将任务划分为同步任务和异步任务,还对异步任务进行了更精细的划分。异步任务又进一步分为微任务和宏任务。
宏任务和微任务分别有各自的任务队列Event Queue,即宏任务队列和微任务队列。
macroTasks(宏任务)包括:
- script(整体代码)
- setTimeout、setInterval、setImmediate(定时器)
- I/O操作(如文件读取、网络请求等)
- AJAX请求
- history traversal(浏览器历史记录的前进和后退)
- requestAnimationFrame
microTasks(微任务)包括:
- Promise.then
- MutationObserver
- process.nextTick(Node.js环境)
- MutationObserver(DOM变动监听)
- Object.observe(对象观察)
NOTE
微任务通常比宏任务更快地执行,因为它们在当前宏任务执行完毕后立即执行,而不需要等待下一个宏任务。
Event Loop执行过程
了解到宏任务与微任务过后,我们来学习宏任务与微任务的执行顺序。
- 代码开始执行,创建一个全局调用栈,script作为宏任务执行
- 执行过程过同步任务立即执行,异步任务根据异步任务类型分别注册到微任务队列和宏任务队列
- 同步任务执行完毕,查看微任务队列
- 若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)
- 若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空