JavaScript异步编程【上】 -- 同步和异步、事件循环(EventLoop)、微任务和宏任务、回调函数

文章内容输出来源:拉勾教育 大前端高薪训练营

前言

在我们学习JavaScript中,我们知道,JavaScript的执行环境是单线程的。所谓单线程是指一次只能完成一个任务,如果有多个任务,就必须排队,只有当前面一个任务完成时,才能执行后面一个任务,以此类推。
在这里插入图片描述
这种模式虽然实现起来比较简单,执行环境相对单纯;但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。

代码如下(示例):

let i = 0;
while (i < 10000) {
	console.log(i)
	i++
}
console.log('aaaa')

上面的例子中,只有等到while循环语句执行完成后,才会执行下面的代码。这种很有可能会因为while语句的执行时间过长,导致整个页面卡在这个地方,其他任务无法执行。

一、同步 和 异步

为了解决上面的问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

  • 同步

上面代码中演示的,只有等前一个任务结束以后,后一个任务才可以执行,程序的执行顺序与任务的排列顺序是一致的、同步的,这就是同步。这种模式所执行的任务被称为同步任务。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

  • 异步

“异步"与“同步”是两种完全不同的执行模式,每一个任务有一个或多个回调函数(callback),当前一个任务结束后,此时不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就会执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。这种模式所执行的任务被称为异步任务。
异步任务指的是,不进入主线程、而进入"任务队列”(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

  • 异步执行的运行机制(EventLoop)

下面我们具体来说一下,异步执行的运行机制。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

1、所有同步任务都在主线程(由JS引擎维护,用来负责解释和执行JavaScript代码)上执行,形成一个执行栈(execution context stack)。
2、主线程之外,还存在一个"消息队列"(queue)(特点:先进先出)。只要异步任务有了运行结果,就在"消息队列"之中放置一个事件。
3、一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"消息队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
4、主线程不断重复上面的第三步。

下图就是主线程和消息队列的示意图。

在这里插入图片描述
只要主线程空了,就会去读取"消息队列",这就是JavaScript的运行机制。这个过程会不断重复。

讲完了异步执行的运行机制,我们还需要了解两个概念:微任务和宏任务。

二、微任务(microtask) 和 宏任务(macrotask)

一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调函数等。除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务。

ES6 规范中,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主(Node、浏览器)发起的,而微任务由JavaScript自身(JS引擎)发起。

任务队列和微任务队列的区别:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候,运行时都会执行队列中的每个任务。在每次迭代开始之后,加入到队列中的任务需要在下一次迭代开始之后才会被执行。
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是,它会等到微任务队列为空才会停止执行 —— 即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始之前且当前事件循环结束之前执行完所有的微任务。

产生宏任务的方式

  • script 中的代码块
  • setTimeout()
  • setInterval()
  • setImmediate()(非标准,IE 和 Node.js 中支持)
  • 注册事件

产生微任务的方式

  • Promise
  • MutationObserver
  • queueMicrotask()

代码如下(示例):

queueMicrotask(() => {
	console.log('微任务')
})

何时使用微任务

微任务的执行时机,晚于当前本轮事件循环的 Call Stack(调用栈)中的代码(宏任务),早于事件处理函数和定时器函数。

使用微任务的最主要原因简单归纳为:

  • 减少操作中用户可感知到的延迟
  • 确保任务顺序的一致性,即便当结果或数据是同步可用的
  • 批量操作的优化

下面我们来看一个批量操作的例子。

代码如下(示例):

let messageQueue = []
let sendMessage = message => {
	messageQueue.push(message)
	if (messageQueue.length === 1) {
		queueMicrotask(() => {
			const json = JSON.stringify(messageQueue);
			console.log(json);
		})
	} 
}

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

sendMessage('刘备')
sendMessage('关羽')
sendMessage('曹操')
sendMessage('张飞')

在上面的代码中,其实整部分代码都是在 Call Stack(调用栈)中的。

  • 首先,第一次调用sendMessage()函数时,此时messageQueue.length === 1是成立的,因此会将queueMicrotask()这部分的代码放置到 “消息队列” (Queue)中。而在上面我们也讲到queueMicrotask()是微任务,因此,我们不会先执行这段代码,而是继续进入调用栈,查看主线程中是否还有其他的同步任务。
  • 然后,第二次执行调用sendMessage()函数时,由于 queueMicrotask()未执行,而messageQueue.length变为了2,因此不会继续将 微任务queueMicrotask()二次加入到“消息队列” (Queue)中。
  • 接着,第三次调用sendMessage()函数,将会重复第二次调用sendMessage()函数的步骤。
  • 上面的步骤会被重复执行,直到主线程中的所有同步任务被执行完成,才会进入 “消息队列” (Queue)中执行 queueMicrotask()。在整个执行过程中,queueMicrotask()其实只进入了一次 “消息队列” (Queue)。

讲到这里,对于程序的执行顺序我们也就有了一定的了解。

那么,接下来我们来看看该如何处理异步问题?

三、回调函数

在JavaScript中,我们会使用回调函数处理异步问题,那么什么是回调函数呢?

回调函数是所有异步编程方案的根基。回调函数就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。这个过程就叫做回调。

代码如下(示例):

function getData (callback) { // 此时函数作为参数传入(函数是一等公民的特性之一)
	setTimeout(function () { // 此时是异步操作,当时间到达一秒以后,
		callback()     // 执行callback回调函数,运行传进来的print函数中的程序,打印
	}, 1000)
}

function print () {
	console.log('执行完定时器后,打印')
}

getData(print)  // 此时传进去的是一个指向print对象的指针,函数名是指针

在上面的代码中,只有一次简单的回调,而在实际应用中,我们可能需要调用很多个回调函数。比如有时可能下一个程序的执行,需要上一个程序的运行结果,此时,我们就必须先等到上一个程序执行完成,才能继续往下执行,这就有可能出现下面代码的情况。

代码如下(示例):

$.get('/url1', function (data1) {
	$.get('/url2', function (data2) {
		$.get('/url3', function (data3) {
			...... 
		})
	})
})

在上面的代码中,出现了很明显的回调嵌套。当我们想要进行调试时,我们不得不从一个函数跳到下一个,再跳到下一个,在整个代码中跳来跳去以查看流程,而最终的结果藏在整段代码的中间位置。真实的JavaScript程序代码可能要混乱的多,使得这种追踪难度会成倍增加。这就是我们常说的回调地狱(Callback Hell)。

那么,为什么会出现这种情况呢?

如果某个业务,依赖于上层业务的数据,上层业务又依赖于更上一层的数据,我们还采用回调的方式来处理异步的话,就会出现回调地狱。

那么,如何解决异步操作所带来的一系列问题呢?

详情请查看【JavaScript异步编程【中】 – Promise 详细解析

参考
JavaScript异步编程
Javascript异步编程的4种方法

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值