前言
以下为部分javascript的知识点整理,我将通过知识点+使用场景进行描述。
事件循环机制(Event Loop)
JavaScript的特点就是单线程,这就意味着当某一个模块需要运行较长时间时,会导致后面的代码处于长时间排队中而无法执行。为了解决这个问题,便有了事件循环机制(event loop),这是javascript基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务
基本的执行机制
主线运行时,会产生堆(heap)和栈(stack),函数调用会生成若干帧组成的栈,当执行完一帧,这一帧出栈,直至所有帧都弹出,栈清空。而在每帧执行当中,JavaScript会遇到一些待处理的事件消息,待处理的消息会被进入消息队列中,在栈清空后,则开始执行队列中的消息,被处理的消息,会被移除队列调用与之关联的函数,此时,函数的调用如前所讲,会形成一个帧并压入栈中,重复如上步骤。直至栈跟队列全部清空。
在消息队列中,setTimeout 具有延迟性,基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间,setInterval同理。即便延迟时间为0,setTimeout也不会立即执行。从这个层面来说,即便promise也是异步,也会先于setTimeout与setInterval执行回调函数。
参考地址:并发模型与事件循环
注:栈的执行规则:先进后出; 队列执行规则:先进先出;
微任务和宏任务
深入来说,我们通常也把js的执行分为同步任务和异步任务,所有同步任务会被压入执行栈中执行,异步任务则放入任务队列,任务队列也分为了微任务队列 和 宏任务队列
宏任务
- 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个
<script>元素中运行代码)。 - 触发了一个事件,将其回调函数添加到任务队列时。
- 执行到一个由
setTimeout()或setInterval()创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。
常见的函数:
setTimeout, setInterval, setImmediate, requestAnimationFrame
微任务
JavaScript 中的 promises和 Mutation Observer API 都使用微任务队列去运行它们的回调函数,我们可以理解为promises和 Mutation Observer API即将运行的回调函数即为一个个微任务
常见到的函数:
process.nextTick, Promises, Object.observe, MutationObserver
执行机制
- 执行栈中的同步任务,如果存在微任务,则该任务进入微任务队列中;如果存在宏任务,则该任务进入宏任务队列中,直至执行栈清空
- 在执行栈清空后,检查微任务队列中的微任务,有则出列调用与之关联的函数,形成一个帧入栈,跳至步骤1,直至微任务队列为空,跳至步骤3;
- 检查宏任务队列中的宏任务,有则出列调用与之关联的函数,形成一个帧入栈,跳至步骤1,直至宏任务队列为空
举例
<script>
console.log('script1')
$('.btn').on('click', function () {
console.log('script1-click')
})
function func3() {
console.log('script1-func3-1')
setTimeout(() => {
console.log('script1-settimeout1')
})
setTimeout(() => {
console.log('script1-settimeout2')
}, 100)
new Promise(res => {
console.log('script1-promise')
res()
}).then(() => {
console.log('script1-promise.then1')
}).then(() => {
console.log('script1-promise.then2')
})
$('.btn').click()
console.log('script1-func3-2')
}
function func2() {
console.log('script1-func2')
func3()
}
function func1() {
console.log('script1-func1')
}
func1()
func2()
</script>
<script>
console.log('script2')
new Promise(res => {
res()
}).then(() => {
console.log('script2-promise')
})
setTimeout(() => {
console.log('script2-settimeout')
})
</script>
如上例子
<script>元素中运行代码分别进入任务队列,开始执行第一个<script>元素中的代码,当func1()被调用时,func1会被压入栈中;执行func1函数时,调用func2(),func2进栈,此时func2中调用func3(),func3进栈;在执行func3时,遇到0延迟即将执行的setTimeout,添加一条消息进入消息队列;继续执行,遇到了延迟100毫秒的setTimeout,未触发事件的发生,不入队;继续执行,遇到了即将执行的promise的状态回调函数,添加一条消息进入消息队列。继续执行,调用click(),click进栈,click执行完成,click出栈;func3执行完成,func3出栈;func2执行完成,func2出栈;func1执行完成,func1出栈。- 此时执行栈为空,检查微任务队列,依次出列执行
promise的回调函数,直至微任务队列清空 - 检查宏任务队列,第二个
<script>元素中的代码开始执行,与上同理
由上可知运行结果:
参考地址:
在 JavaScript 中通过 queueMicrotask() 使用微任务
JavaScript 运行机制详解:再谈Event Loop
日常可见的运用:
利用setTimeout做防抖与节流,控制请求的发送
vue中节点的异步渲染,以及vue中封装的nextTick回调函数执行属于微任务,先于setTimeout执行
问题
以下问题为我从文档中了解后的个人理解,如有误,欢迎指正
-
如点击事件click入不入任务队列?
如果事件本身有绑定回调函数,用户在点击时,即表示这是即将需要执行的任务,会入队列中。但在上面的例子中,我们可以知道,但js代码中直接调用click(),此时不会进入任务队列,因为此时是函数的直接调用,直接形成一个帧入栈
-
promise为什么有部分代码是同步执行的,如下例子,
script1-promise是同步打印的?我的理解是,new 一个函数,本身就是函数的构造调用,从上面我们可以知道,函数的调用会入栈,但promise的状态处理回调函数是异步的,且属于微任务
new Promise(res => {
console.log('script1-promise')
res()
})
小结
- 执行栈用来执行同步任务,函数的调用会被压入执行栈(先进后出)
- 执行栈清空,微任务队列里的微任务依次出列执行(先进先出),直至队列清空
- 微任务队列清空后,宏任务队列取出一个任务执行,进入1,开始一次新的事件循环
如有错误缺漏,欢迎留言指正