彻底搞懂JavaScript单线程异步执行机制

彻底搞懂JavaScript单线程异步执行机制

  • 关于JavaScript异步
    • ajax
    • setTimeout
    • setInterval
    • Promise
    • 宏任务vs微任务
  • 结束语

关于JavaScript异步

JavaScript是一门单线程语言 一切JavaScript中说的多线程都是纸老虎 本质都是用单线程模拟出来的

先来一张来自大神(mr.z 大佬)的图

本文的目的就是彻底弄懂JavaScript的事件循环的执行机制。

首先,不论你是面试求职,还是日常的开发工作,给定的几行代码,我们需要准确的知道代码执行输出的结果,也就是代码执行的顺序。因为JavaScript是单线程语言,所以它的语句都是顺序执行的,也就是同步任务。但是,如果加载一个包含了很多图片的页面,如果都是同步任务,要是图片资源巨大加载不出来,岂不是后面的内容都显示不了。所以就要了解JavaScript的事件执行机制。
其实,当打开页面时,页面的渲染过程涉及到很多同步任务(页面骨架,页面元素的渲染),也有一些异步任务(图片音乐之类占用资源大耗时比较久的任务)。

  • 同步任务
  • 异步任务

同步任务和异步任务会在不同的执行场所等待被执行,同步任务会在主线程中执行,碰到异步任务会先放到异步队列中等待被执行。

下图用来说明每当js遇到事件时会如何执行
彻底搞懂JavaScript单线程异步执行机制_第1张图片

ajax

AJAX(Asynchronous Javascript And XML)异步JavaScript和XML,相信我们在日常工作中ajax已经用过很多了,通过和后端进行少量数据交互,实现网页异步更新。
ajax出现之前,如果网页需要更新部分数据,比如我们获取一个表单数据,就必须重载整个网页页面。
有了ajax技术,我们就可以在不重载整个网页的情况下,对网页的某部分进行更新,比如只更新表单数据,而不用对表单整个页面进行重载。
看一个例子:

$.ajax({
	url: 'xxx.com/list',
	data: xxx,
	success: () => {
		console.log('接收数据了')
	}
})
console.log('代码执行结束')

这一段简单的ajax请求代码就是个简单的异步任务:

  1. ajax进入事件列表,注册回调函数success
  2. 执行主线程同步任务console.log('代码执行结束')
  3. ajax事件完成,回调函数success进入事件队列(Event Queue)
  4. 主线程从事件队列读取回调函数success并执行

setTimeout

setTimeout也是异步任务。那到底是如何异步执行的呢?

通常我们延时执行时会想到用setTimeout

setTimeout(() => {
	console.log('延时1s')
}, 1000)

逐渐用的多了,问题也多了,有时候延时1秒但是实际却过了3,4秒才执行。比如下面的例子

setTimeout(() => {
	task()
}, 1000)
console.log('主流程console')
sleep(100000) // 执行了很久的函数

我们知道setTimeout是个异步任务,这里我们想要task在1秒后执行,但是实际等待的时间却远远不止1秒,这是为什么?我们弄懂这里的setTimeout异步任务是如何执行的就明白了:

  1. 首先遇到setTimeout,发现是个异步任务时,会注册其回调函数task
  2. 执行主流程console
  3. 执行主流程任务sleep函数,执行了很久很久…
  4. 3秒后,回调task进入到事件队列中(如果这时候主线程任务都执行完了是空闲的,下一步主线程就会从事件队列中取到这个函数去主线程中执行了)。此时应该开始执行事件队列中注册的task了,但是主线程还在执行sleep,因为js是单线程,所以只能继续等待
  5. sleep终于执行完了,task从事件队列中取出进入到主线程中执行

上述流程了解完,我们就知道setTimeout的延时时间,是指定经过多长时间把要执行的任务加入到事件队列中去,所以真实等待的时长就不仅仅是根据setTimeout的延时时间参数来决定的了,还依赖主线程中任务执行的时长。

setInterval

setIntervalsetTimeout很像,它的含义是指定多长时间将注册的函数放入事件队列中,如果前面的任务耗时很长,同样也会延长这个时间。通过对setTimeout的说明,知道对setInterval(fn, ms)的理解需要注意的是:不是每隔ms执行一次fn,而是每隔ms,会将fn加入到事件队列中一次。
所以,细品这句话:如果fn执行时间超过了ms,用户是看不出这个时间间隔的。因为,如果你每隔1秒加入一个fn,而这个fn每次执行都需要5秒才能结束,也就是主线程第一个执行完的时候去查队列时,发现队列中还有4个没执行呢,此时主线程就又会去队列中拿一个新的fn到主线程中去执行,也就没有什么休息时间了。

Promise

ES6为我们提供了Promise来处理异步操作。

先看一个例子:

setTimeout(() => {
  console.log('setTimeout')
})

new Promise((resolve) => {
  console.log('promise')
  resolve()
}).then(() => {
  console.log('then')
})

console.log('console')

根据之前的介绍,如果promisesetTimeout的事件循环机制相同的话,结果应该是

// promise
// console
// setTimeout
// then

但是正确的执行结果却是:

// promise
// console
// then
// setTimeout

宏任务vs微任务

所以我们知道,虽然都是异步任务,但是promisesetTimeout却是不同的异步任务,异步任务有两种

  • 宏任务(scriptsetTimeoutsetInterval,UI渲染,I/OpostMessage等)
  • 微任务(promiseprocess.nextTick

不同的任务也会进入不同的队列中,循环任务执行时也会不同。上述的例子具体的执行步骤:

  1. 因为整段代码在script中,所以首先整段代码作为一个宏任务,进入主线程
  2. 遇到setTimeout,此时将其回调函数注册后发布到事件队列中
  3. 接下来遇到promise,因为我们知道promise是立即执行的(可参考Promise原理和使用),而then回调会分发到微任务队列中
  4. 立即执行console
  5. 到此为止第一轮宏任务完成。开始检查微任务队列中是否有回调,发现了then,执行
  6. 到此第一个事件循环结束了。
  7. 第二轮事件循环开始,从宏任务队列中查看,发现了setTimeout的回调,执行。
  8. 没任务了,结束。

所以,宏微任务的关系如下:
彻底搞懂JavaScript单线程异步执行机制_第2张图片
最后一个复杂的例子,检查你是否掌握了这个执行机制:

console.log(1)

setTimeout(() => {
  // setTimeout_1
  console.log(2)
  new Promise((resolve) => {
    console.log(3)
    resolve()
  }).then(() => {
    // promise_3
    console.log(4)
  })
}, 1000)

new Promise((resolve) => {
  console.log(5)
  resolve()
}).then(() => {
  // promise_1
  setTimeout(() => {
    // setTimeout_3
    console.log(6)
  })
  console.log(7)
})

setTimeout(() => {
  // setTimeout_2
  console.log(8)
  new Promise((resolve) => {
    console.log(9)
    resolve()
  }).then(() => {
    // promise_2
    console.log(10)
  })
})

第一轮事件循环:

  1. 整体代码作为script宏任务,遇到console.log(1)
  2. 遇到setTimeout回调1秒后放入宏事件队列,记为setTimeout_1
  3. promise立即执行,console.log(5)同时then放入微任务中,记为promise_1
  4. 又遇到setTimeout,立即放入宏任务队列中,记为setTimeout_2
  5. 此时宏任务完成,去微任务队列中看看这一轮宏任务产生的微任务有哪些,发现只有一个promise_1,执行时候发现又遇到一个宏任务,将其放入宏任务队列中,记为setTimeout_3,继续执行console.log(7)
  6. 过了1秒了,setTimeout_1也被加入到宏队列中了
  7. 第一轮循环结束,结果:1,5,7,此时任务队列
宏任务
setTimeout_2
setTimeout_3
setTimeout_1

第二轮事件循环:

  1. 拿到宏任务队列中setTimeout_2,执行console.log(8),立即执行promiseconsole.log(9),同时将其then加入到微任务中,记为promise_2
  2. 宏任务完成,去微任务队列中发现promise_2,执行console.log(10)
  3. 此时第二轮循环结束,结果为:1,5,7,8,9,10

第三轮事件循环:

  1. 拿到宏任务setTimeout_3console.log(6),无微任务,结束
  2. 此时结果为:1,5,7,8,9,10,6

第四轮事件循环:

  1. setTimeout_1立即执行console.log(2),遇到promise立即执行console.log(3),同时将then加入到微任务中,记为promise_3
  2. 宏任务结束,查看微任务队列,执行promise_3,执行console.log(4)
  3. 本轮循环结束,结果为:1,5,7,8,9,10,6,2,3,4

结束语

从头到尾都在强调JavaScript是一门单线程语言,不管是什么新框架还是新语法糖实现的所谓的异步,其实都是根据JavaScript的事件循环机制原理用同步的方式来模拟的,事件循环是JavaScript实现异步的一种方法,也是它的执行机制。
牢牢记住这两点:

  • JavaScript是一门单线程语音
  • 事件循环是它的执行机制
  • 微任务还是宏任务针对的是回调函数,当前的new Promise或者定时器是同步立即执行的,异步的是他们的then和回调函数方法

你可能感兴趣的:(JS,javascript,前端,ajax)