JavaScript 异步编程(1)单线程与事件循环

主流的 JavaScript 环境都是以单线程模式执行 JavaScript 代码。

单线程:JavaScript 执行环境中负责执行代码的线程只有一个。

采用单线程模式工作的原因

和设计初衷有关,JavaScript 最初仅仅是浏览器端的脚本语言,目的是为了实现页面上的动态交互。

页面交互的核心是 DOM 操作,这决定了 JavaScript 必须使用单线程模型,否则会出现线程同步问题。

线程同步问题:若 JavaScript 同时有多个线程工作,某个线程修改了 DOM 元素,另外一个线程同时又删除了这个元素。浏览器无法明确以哪个线程的结果为准。

  • 优点:安全、简单
  • 缺点:如果遇到特别耗时的任务,后面的任务需要排队等待,这会导致程序的执行被拖延,出现假死的情况。
console.log('foo')

for (let i = 0; i < 100000; i++) {
	console.log('耗时操作')
}

console.log('等待耗时操作结束')

为了解决耗时任务阻塞执行的问题,JavaScript 将任务的执行模式分成了两种:

  • 同步模式(Synchronous)
  • 异步模式(Asynchronous)

我们重点学习的便是异步模式。

内容概要

  • 同步模式与异步模式
  • 事件循环与消息队列
  • 异步编程的几种方式
  • Promise 异步方案、宏任务/微任务队列
  • Generator 异步方案、Async / Await 语法糖

同步模式 Synchronous

大多数任务都会以同步模式执行。

console.log('global begin')

function bar () {
	console.log('bar task')
}

function foo () {
	console.log('foo task')
  bar()
}

foo()

console.log('global end')

JavaScript 调用栈

JavaScript 有一个调用栈(Call Stack),会记录当前正在做的事情。

以上面的代码为例,详细解释调用栈的工作流程。

调用栈的工作流程

开始时会将整块代码作为匿名函数压入调用栈。
image.png
然后开始逐行执行代码。

执行过程中,控制台打印 “bar task”,执行完之后,console.log 调用结束,弹出调用栈,继续往下执行。
image.png
函数和变量的声明不会产生任何调用,继续往下执行。

当遇到 foo 函数调用时,函数调用会压入调用栈,并开始执行 foo 函数。
image.png
foo 函数内先是 将 console.log 调用栈执行。打印完之后再将 bar 函数压入调用栈,并执行函数内的代码。
image.png
bar 函数内同样将 console.log 压入调用栈,执行,弹出调用栈,foo 函数执行结束,从调用栈中弹出。

最后将 console.log 压入调用栈,执行,弹出调用栈后,整体的代码全部结束,调用栈被清空。

当调用栈全部清空时,这一轮的工作就结束了。

存在问题

如果某一个任务执行时间过长,后面的任务会延迟,这称之为阻塞。

这种阻塞对用户而言,界面会出现卡顿或卡死现象。

必须使用异步模式解决程序中无法避免的耗时操作,如浏览器端的 AJAX 操作,或者 Node.js 端的大文件读写。

异步模式

异步模式不会等待这个任务结束才开始下一个任务。异步模式开启过后就立即往后执行下一个任务。

后续逻辑一般会通过回调函数的方式定义。

如果没有异步模式,单线程的 JavaScript 语言就无法同时处理大量耗时任务。

对于开发者而言,异步模式的代码执行顺序混乱,我们需要多看、多练、多思考。

我们以一段代码为例,分析异步模式的执行顺序。

console.log('global begin')

setTimeout(function  timer1 () {
	console.log('timer1 invoke')
}, 1800)

setTimeout(function  timer2 () {
	console.log('timer2 invoke')
  
  setTimeout(function  inner () {
    console.log('inner invoke')
  }, 1000)
}, 1000)

异步调用的过程相对复杂一些,我们需要了解一些概念

  • Web APIs
  • 事件循环 Event loop 和消息队列 Queue

异步调用的实现过程

加载整体代码,在调用栈中压入一个匿名的全局调用,依次执行每行代码。
image.png
对 console.log 这样的同步 API,仍然是压栈、执行 -> 打印、弹栈。

在调用 setTimeout 部分代码时,仍然是先压栈,因为 timer1 是异步调用,Web APIs 开启了一个 timer1 倒计时器,倒计时器是单独工作的,不受当前 JS 线程影响。

image.png
开启倒计时器过后,setimeout 调用执行完了,会被弹出调用栈,代码继续往下执行。
代码往下执行时,又遇到 settimeout,同样是先压栈,开启 timer2 倒计时器,弹栈。
image.png
然后又遇到 console.log,同样压栈、执行、弹栈,整体代码执行完毕,匿名函数弹栈,调用栈清空。
image.png
此时,调用栈 Call stack 已经不再工作。

Event loop 负责监听调用栈和消息队列。

当调用栈没有任务之后,Event loop 会从消息队列中取出第一个回调函数压入调用栈,但此时消息队列为空,因此并不会执行操作。

Web APIs 两个倒计时器开启后计时会不断减少,timer2 倒计时器会先结束,便会被放到消息队列 Queue,timer1 倒计时器结束后也会放到 Queue。
image.png
一旦消息队列中发生变化,事件循环 Event loop 监听到后会将消息队列中的第一个回调函数 timer2 压入调用栈 Call stack,调用栈开启新一轮执行,执行过程与之前类似。
image.png
如果这个过程又遇到了异步调用,仍会开启倒计时器。
image.png
我们可以将调用栈 Call stack 认为是一个正在执行的工作表,Queue 是一个代办的工作表。

JavaScript 执行引擎先做完调用栈中 Call stack 的任务,然后再通过事件循环 Event loop 从消息队列 Queue 中取一个任务继续执行。我们可以往消息队列中添加任务,消息队列中的任务会排队等待事件循环 Event loop。

这里为了方便理解步骤有先后顺序,实际执行时有各自的 timeline,如倒计时器开启后便会自动倒计时,不会管 Call stack 和 Queue 的情况,如下图能够更好的描述 JavaSCript 的执行情况。
image.png
JavaScript 是单线程的,但是浏览器不是单线程的。内部的 API 会用单独的线程执行等待的操作。

这里的同步或异步模式指运行环境提供的 API 是以同步或异步模式的方式工作。

回调函数

回调函数是所有异步编程方案的根基。

什么是回调函数

image.png
以程序中的 Ajax 为例,当我们去调用 Ajax 操作,目的是为了拿到请求结果后进行处理,如显示在界面上。

我们不清楚请求在何时完成,所以我们需要将响应后需要执行的任务定义到一个函数之中,内部的 Ajax 请求完成之后自动执行这个任务。

这种由调用者定义,交给执行者执行的函数称之为回调函数。

function foo (callback) {
	setTimeout(function () {
  	callback()
  }, 3000)
}

foo(function () {
  console.log('这就是一个回调函数')
  console.log('调用者定义这个函数,执行者执行这个函数)
  console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})

回调函数不易于阅读,执行顺序较为混乱。

除了传递回调函数参数的方式,还有事件机制、发布订阅,其实也是基于回调函数的一些变体。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值