本篇主要讨论的是JavaScipt的事件循环机制(event loop),主要分为以下几个部分:
- 为什么需要事件循环机制
- 事件循环机制的表现
- 有哪些使用场景
- nodejs的事件循环(待补充)
为什么需要事件循环机制
众所周知,在浏览器中,JavaScript
是单线程的(原因在此处不做过多赘述),也就是说所有的事件都在一个线程中执行,这个线程负责处理:
- 用户交互事件
- DOM元素更新、绘制
- 执行js代码
- …
我们知道,当我们调用一个方法的时候,js 会生成一个与这个方法对应的执行环境(context
),又叫执行上下文。
这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 当一系列这样的方法被依次调用的时候,js 会将他们放入 执行栈 中依次执行(排队)。
如果某个任务执行时间过长,后面的任务则会一直处于等待状态,任务执行效率低且用户体验也不好,所以有了异步任务
& 事件循环机制
。
在 js 中,常用的异步编程方式有回调函数、Promise 和 async/await 等。这些方式都可以让 js 代码异步执行,从而避免长时间阻塞 UI 线程。而异步任务的关键就在于任务队列,当异步任务开始执行的,会放入这个任务队列,执行完成之后,再从任务队列弹出,这个下面详细说明。
事件循环机制的表现
js中任务主要分为:
- 同步任务:马上可以拿到结果,耗时较短
- 异步任务:不知道什么时候可以拿到结果,时间未知
当 执行栈 中出现了异步任务是一件很痛苦的事,所以我们可以不看让异步任务另外排队呢,这样也不影响同步任务的执行效率,是的,于是js中有了事件队列(Task Queue)的概念:
js引擎遇到一个异步任务后并不会一直等待其返回结果,而是会将这个任务挂起,继续执行执行栈中的其他任务。当一个异步任务返回结果后,js会将这个任务加入另外一个队列,我们称之为事件队列,事件队列中的任务不会马上执行,需要等到执行栈中的同步任务全部执行完成之后,主线程轮训查看事件队列中是否有任务需要执行,是的轮训,因为代码执行过程中会有新的 同步任务/异步任务 排进执行栈,所以主线程会一直在执行栈跟任务队列中轮训,其实这大概就是事件循环的机制了,整体流程大概如下图所示。
如果在异步任务完成或放入执行栈执行,有新的同步任务产生,则会导致异步任务延迟执行。比如可能会导致 setTimeout
不准
但是,还没完。。。
异步任务分为很多种且执行优先级不一样,所以对于异步任务又做了细分:宏任务、微任务
宏任务包括有
- Http请求、
- setTimeout
- setInterval
- requestAnimationFrame
微任务包括有:
- promise.then
- promise.catch
- promise.finally
- MutationObserver
微任务一定优先于宏任务
微任务是线程之间的切换,速度快,不需要进行上下文切换
宏任务是进程之间的切换,速度慢,每次执行需要切换上下文
上面提到过,异步任务返回结果后会被放入事件队列中。然而,为了细分宏任务跟微任务的执行顺序,会根据任务类型将任务放入对应的宏任务队列或微任务队列。
当在执行栈为空的时候,主线程会查看微任务队列是否有事件存在。
- 不存在,会在宏任务队列中取出一个任务放入执行栈
- 存在,执行微任务队列中的任务,直至为空,然后在宏任务队列中取出一个任务放入执行栈
等待执行栈的任务完成/清空后,再依次检查 微任务队列、宏任务队列等等
以上就是JavaScript
的事件循环机制
1 | // setTimeout设置为0时也属于宏任务 |
注意:在每个循环的末尾,会进行一次dom的更新渲染,vue中$nextTick与此有异曲同工之处,或者说是借鉴了这里
有哪些使用场景
- 使用
setTimeout
对不重要的事件,同步任务异步化,避免主线程阻塞 - $nextTick的实现:$nextTick内部实现优先级:微任务setImmediate、微任务MessageChannel、宏任务setTimeout、Promise.then
- 待补充
nodejs事件循环
待补充