这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战
javascript是一门单线程语言,是按照语句出现的顺序执行的,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等待。为了解决这个问题,将任务分为两类:
- 同步任务
- 异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。 下面是图片说明:
当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境,这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。 当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码。如此反复,这样就形成了一个无限的循环。
setTimeout
我们经常这么实现延时1秒执行:
setTimeout(() => {
console.log('延时1秒');
},1000)
setTimeout是异步操作首先进入event table, 注册的事件就是它的回调,触发条件就是1秒之后,当满足条件回调被推入event queue,当主线程空闲时会去event queue里查看是否有可执行的任务。
console.log(1) // 同步任务进入主线程
setTimeout(getData(),0) // 异步任务,被放入event table, 0秒之后被推入event queue里
console.log(3) // 同步任务进入主线程
console.log(1) ,console.log(3)是同步任务马上会被执行,执行完成之后主线程空闲去event queue(事件队列)里查看是否有任务在等待执行,这就是为什么setTimeout的延迟事件是0毫秒却在最后执行的原因
宏任务,微任务
- 宏任务 包含整个script代码块,setTimeout, setIntval
- 微任务 Promise , process.nextTick
事件循环机制
不同类型的任务会进入对应的event queue, 比如setTime和setIntval会进入相同(宏任务)的event queue, 而Promise和process.nextTick会进入相同(微任务)的event queue.
Promise与事件循环
Promise在初始化时,传入的函数是同步执行的,然后注册then回调。注册完之后,继续往下执行同步代码,在这之前,then的回调不会执行。同步代码块执行完毕后,才会在事件循环中检测是否有可用的promise回调,如果有,那么执行,如果没有,继续下一个事件循环。
1. 宏任务,微任务都是队列, 一段代码执行时,会先执行宏任务中的同步代码。
2. 进行第一轮事件循环的时候会把全部的js脚本当成一个宏任务来运行。
3. 如果执行中遇到setTimeout之类的宏任务,那么就把这个setTimeout内部的函数推入[宏任务的队列]中,下一轮宏任务执行时调用。
4. 如果执行中遇到promise.then()之类的微任务,就会推入到[当前宏任务的微任务队列]中, 在本轮宏任务的同步代码都执行完成后,依次执行所有的微任务。
5. 第一轮事件循环中当执行完全部的同步脚步以及微任务队列中的事件,这一轮事件循环就结束了, 开始第二轮事件循环。
6. 第二轮事件循环同理先执行同步脚本,遇到其他宏任务代码块继续追加到[宏任务的队列]中,遇到微任务,就会推入到[当前宏任务的微任务队列]中,在本轮宏任务的同步代码执行都完成后, 依次执行当前所有的微任务。
7. 开始第三轮循环往复..
例题
console.log('1')
setTimeout(function() {
console.log('2')
process.nextTick(function() {
console.log('3')
})
new Promise(function(resolve) {
console.log('4')
resolve()
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6')
})
new Promise(function(resolve) {
console.log('7')
resolve()
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9')
process.nextTick(function() {
console.log('10')
})
new Promise(function(resolve) {
console.log('11')
resolve()
}).then(function() {
console.log('12')
})
})
结果为1,7,6,8,2,4,3,5,9,11,10,12
解析:
1.整体script作为第一个宏任务进入主线程,遇到console.log(1)输出1
- 遇到setTimeout, 其回调函数被分发到宏任务event queue中。我们暂且记为setTimeout1
2.1.遇到process.nextTick(),其回调函数被分发到微任务event queue中,我们记为process1
2.2.遇到Promise, new Promise直接执行,输出7.then被分发到微任务event queue中,我们记为then1 - 又遇到setTimeout,其回调函数被分发到宏任务event queue中,我们记为setTimeout2.
- 现在开始执行微任务, 我们发现了process1和then1两个微任务,执行process1,输出6,执行then1,输出8, 第一轮事件循环正式结束, 这一轮的结果输出1,7,6,8.那么第二轮事件循环从setTimeout1宏任务开始
- 首先输出2, 接下来遇到了process.nextTick(),统一被分发到微任务event queue,记为process2
8.new Promise立即执行,输出4,then也被分发到微任务event queue中,记为then2 - 现在开始执行微任务,我们发现有process2和then2两个微任务可以执行输出3,5. 第二轮事件循环结束,第二轮输出2,4,3,5. 第三轮事件循环从setTimeout2哄任务开始
10。 直接输出9,跟第二轮事件循环类似,输出9,11,10,12 - 完整输出是1,7,6,8,2,4,3,5,9,11,10,12