什么是阻塞?
今天来介绍一下JS的异步机制,废话不多说,下面开干! 说到异步,我们需要先了解一下什么是阻塞。执行下面的代码
const btn = document.getElementById("btn")
btn.addEventListener('click', () => {
let startTime = new Date().getTime() // 记录开始时间
let temp = 0
for (let i = 0; i < 10000000000; i++) {
temp += 1
}
let endTime = new Date().getTime()
console.log(`for循环执行了:${endTime - startTime}ms`, temp)
const el = document.createElement('p')
el.innerText = '我是新增加的元素'
document.body.appendChild(el)
})
// 输出 for循环执行了:9554ms 10000000000
然后我们观察页面会发现,在点击了按钮之后页面就像“卡”住了一样,新增加的p标签都是等了将近10s后才出现,那么问题就是我们执行的for循环阻塞了代码,因为在进行for循环的时候需要花费时间,系统此时正在“全力”算这个for循环,后面的操作都被阻塞。
通过上面的例子我们可以看到,由于阻塞导致的页面卡顿会是用户体验非常差,我们怎么改进呢?首先想到是异步的方法,我们把for转为异步,这个时候,我们可以交互先正常地进行下去,至于结果我们可以在正常交互都结束了,再在“某个时机”得到都行,改写上面的代码
const btn = document.getElementById("btn")
btn.addEventListener('click', () => {
setTimeout(() => {
let startTime = new Date().getTime() // 记录开始时间
let temp = 0
for (let i = 0; i < 10000000000; i++) {
temp += 1
}
let endTime = new Date().getTime()
console.log(`for循环执行了:${endTime - startTime}ms`, temp)
}, )
const el = document.createElement('p')
el.innerText = '我是新增加的元素'
document.body.appendChild(el)
})
// 输出 for循环执行了:9548ms 10000000000
又上面的代码可以看出,我们将for循环的相关逻辑放在了一个serTimeout
函数中,但是此时页面的交互体验却大大提升:点击按钮之后,新增加的一行段落马上就展示出来了,在等了一小会之后,控制台中也打印出相应的输出结果。所以说异步并没有阻塞我们正常的流程。
通过上面的例子我们再深入思考一下,平时我们的页面中有没有什么比较常见需要等待的东西呢?我就直接来说场景吧,那就是网络请求。网络请求的具体流程就不细说了,大致的话我分为了三个阶段,发起请求-->等待响应-->接收响应结果。如果在网络状况好的情况下,可能这个流程1s甚至几十毫秒就完成了,但是网络不好或者存在多个同时发起的请求(chrome最大请求数量6个)会导致等待时间边长。这个时候如果是让用户一直等着,那体验可就太差了。所以AJAX的出现直接让web出现爆炸式的成长,因为有了AJAX之后,网络请求变为异步,用户可以在正常交互的情况下去等待网络响应的结果。那么一句通俗一点的话来总结阻塞就是,在发生阻塞之后,后面的代码根本不执行,你干什么页面都不会立刻响应。
JS的异步机制
从上面我们已经了解到,发生阻塞的情景,以及如果通过异步去解决。现在我们的重点就来来了,在JS中,异步机制是什么样的?
对于JS的异步机制,我们可能还要分开浏览器的异步机制和Node环境的异步机制来讨论。那么我们就先来介绍一下浏览器的异步机制吧。
JS异步第一层
我们先来从同步和异步来进行讲解。我们将所有JS的任务划分为同步任务和异步任务,可以这样来理解JS代码的执行顺序,在JS执行的过程中,我们假设有两个队列,一个是同步任务的队列,一个是异步任务的队列,这两个队列专门用于执行对应类型任务,但是由于JS是单线程的原因,一次只能去处理一个队列里面的任务,所有这两个队列的执行顺序肯定会分先后,那么根据规定,在执行JS代码的时候,先执行同步任务,如果有异步任务,则异步任务直接放入异步队列中,JS继续执行之后同步任务,那么当同步任务全部执行完了,或者说同步队列清空了,这个时候就开始执行异步队列里面的任务,如果执行的异步任务中有同步任务,那么会把同步任务存入同步任务队列之中,重复刚才的步骤。这样说是不是有点绕?我们用一段代码和一张图来进行理解。
console.log('task_1')
console.log('task_2')
setTimeout(() => {
console.log('task_3')
setTimeout(() => {
console.log('task_4')
})
console.log('task_5')
}, 0)
console.log('task_6')
setTimeout(() => {
console.log('task_7')
}, 0)
console.log('task_8')
从第一小节我们可以知道,setTimeout()
是一个异步函数,而console.log()
是一个同步函数,对于上面的代码输出的结果是怎么样的呢?答案是task_1 task_2 task_6 task_8 task_3 task_5 task_7 task_4
不知道大家算的是不是和我一样,那么现在我用文字和图片来解释一下这段代码的执行逻辑。以下用数字来表示任务
-
1,2进入同步队列,遇到异步任务,将整个
setTimeout
代码放入异步队列,6进入同步队列,再次将setTimeout
放入异步队列,8进入同步队列 -
开始打印同步队列,输出1,2,6,8,此时同步队列清空,开始执行异步队列,对于第一个异步任务,打开
setTimeout
之后,发现有两个同步任务一个异步任务,按照上一步方法,将3存入同步队列,4放入异步队列的最后,5进入同步队列。 -
开始打印同步队列,输出3,5,此时异步队列有7,4,开始执行异步队列,将7存入同步队列
-
开始打印同步队列,输出7,此时异步队列只剩一个4,存入同步队列,异步队列清空
-
开始打印同步队列,输出4,此时同步队列和异步队列都清空,任务完成
最终输出结果:1,2,6,8,3,5,7,4。以上是比较基础的JS异步机制,接下来我将更加详细介绍异步任务类型和异步队列
JS异步第二层
通过上面,我们现在知道了同步和异步一个初步的概念,那么大家有没有思考过,为什么setTimeout
是异步函数的?你想一下,假如定时器是一个同步任务会发生什么?那么代码的执行肯定会因为等待定时器而发生阻塞,那么是不是异步才是更好的方案呢?从之前的描述我们知道了异步队列还有setTimeout
是一个异步任务,那么根据这两点再进行一个延伸,异步队列中是什么样的?还有哪些异步任务和其类型?我们先来介绍一下浏览器环境和Node环境中的异步任务有哪些吧
-
浏览器环境常见异步任务
宏任务(macrotask) 微任务(microtask) setTimeout promise.then catch finally setInterval I/O -
Node环境中常见异步任务
宏任务(macrotask) 微任务(microtask) setTimeout promise.then catch finally setInterval process.nextTick setImmediate I/O
看到这里,大家是不是突然发现冒出两个新名词宏任务(macrotask和 微任务(microtask),这两个概念呢主要是区分一下异步任务的类型,那么他们有什么却别呢,简单的说一下就是在异步队列里面又再次被细分成了宏任务队列和微任务队列,微任务队列是先于宏任务队列执行的。我们按照之前同步队列和异步队列的方法来理解宏任务和微任务队列。在同步队列清空后,现在要开始执行异步队列。在执行异步队列的时候,首先执行完微任务队列,如果再次遇到异步任务,则将任务存入相应类型的异步任务队列,微任务清空开始执行宏任务,宏任务中有微任务则将其放入微任务队尾。同样我们还是以一段在浏览器环境中的代码和图片来理解宏任务和微任务。
console.log('task_1')
setTimeout(() => {
console.log('task_2')
setTimeout(() => {
console.log('task_3')
}, 0)
console.log('task_4')
}, 0)
console.log('task_5')
Promise.resolve().then(() => {
console.log('task_6')
})
console.log('task_7')
那么上述代码输出结果是:task_1 task_5 task_7 task_6 task_2 task_4 task_3
,执行过程如下:
-
同步队列1,5,7,微任务队列6,宏任务队列
setTimeout
代码块 -
打印1,5,7,将6存入同步队列,微任务队列清空
-
打印6,现在开始执行宏任务队列,将2,4存入同步队列,
setTimeout
代码块存入宏任务队列 -
打印2,4,继续执行宏任务队列,将3存入同步队列
-
打印3,此时同步队列,微任务队列和宏任务队列都清空,任务结束
最后结果就是1,5,7,6,2,4,3
JS异步第三层
我们在第一层的时候讲解了同步队列和异步队列的概念,在第二层我们讲解了异步队列中的宏任务和微任务。但是在前两层我们构造的模型都是基于JS引擎抽象出来的一个概念模型,那么在第三层我们一起深入V8引擎中探索一下JS运行机制和最有名的Event Loop机制。
由于本文的篇幅已经较长,关于JS的运行机制,我也单独写一篇JS引擎和运行机制来描述。以下是链接。
总结
通过本文,相信大家已经基本上了解到了JS的异步机制和JS引擎的运行原理。JS还有一些概念和模型在之后我们会继续深入了解。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!