10分钟了解JS异步与同步
2024-12-09 11:13:16

模拟面试,感觉自己JS异步同步自己回答的不够精彩,重新整理下。

正文

本文仅算入门浅析,如有说错的部分,还请轻喷。

在正式了解Promise之前,我们需要先了解一些基础知识。

我们知道JavaScript的单线程的,这与它的用途有关。

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。

这决定了它只能是单线程,否则会带来很复杂的同步问题。

比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所谓”单线程”,就是指一次只能完成一件任务。

如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式好处坏处都很明显。

好处是实现起来比较简单,执行环境相对单纯;

坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。

常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

ajax的同步请求就会导致浏览器产生假死,因为它会锁定浏览器的UI(按钮,菜单,滚动条等),并阻塞所有用户的交互,jquery中的ajax有这样一个同步请求的功能,一定要慎用,尤其是在请求的数据量很大的时候,要避免使用同步请求。

举几个栗子感受一下异步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>javascript异步</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>

<body>
<button>点击</button>
<script>
{
let myData = null
//ajax请求
function ajax() {
axios.get('https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock')
.then(data => {
console.log("ajax返回成功");// handle success
myData = data.data
console.log(myData);

})
.catch(error => {
// console.log(error); // handle error
console.log("ajax返回失败");
})
}
}
</script>
</body>
</html>

JS引擎线程

JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程,但是浏览器的渲染进程是提供多个线程的

  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程
  • GUI渲染线程

定时器

1
2
3
4
5
6
7
8
9
10
console.log(myData);
setTimeout(() => {
console.log('定时器');
}, 2000);
console.log(myData);

// 输出,应该没什么悬念
// null
// null
// 定时器d

我们看一下执行顺序

  1. console.log(myData)
  2. 定时器,将定时器挂起(就是暂停了这个定时器)
  3. 继续执行第二个 console.log(myData);
  4. 没有可以执行的js代码,回头把挂起的任务继续执行下去

AJAX

1
2
3
4
5
6
7
8
console.log(myData);
ajax()
console.log(myData);

//null
//null
//ajax返回成功
//{success: true, data: {…}}(这是接口返回的数据,我们不必关心返回的具体内容,只要知道返回了就好,陌上寒注)

定时器与AJAX

1
2
3
4
5
6
7
8
9
10
11
12
console.log(myData);
ajax()
setTimeout(() => {
console.log('定时器');
}, 2000);
console.log(myData);

//null
//null
//ajax返回成功
//{success: true, data: {…}}
//定时器

两个异步函数相遇了,先执行谁?谁跑的快就先执行谁?

当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给 webapi,也就是浏览器提供的相应线程去处。

如定时器线程为setTimeout计时、异步http请求线程处理网络请求,而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞

定时器触发线程也只是为 setTimeout(..., 1000) 定时而已,时间一到,还会把它对应的回调函数(callback)交给 任务队列 去维护,JS引擎线程会在适当的时候去任务队列取出任务并执行。

JS引擎线程什么时候去处理呢?消息队列又是什么?

为了解决这个异步执行顺序的问题,JavaScript 通过 事件循环 event loop 的机制来解决这个问题。

事件循环和任务队列

两个console.log(myData)是同步执行的,他们都在js的主线程上执行

在主线程之外还存在一个任务队列,任务队列中存放着需要异步执行的内容

当主线程运行完毕之后,就会去执行任务队列中的任务(不断的重复扫描)直到任务队列清空

1
2
3
4
5
6
7
8
console.log(1);
setTimeout(function () {
console.log(2);
}, 1000);
console.log(3);

// 输出,这没什么可解释的
// 1,3,2

再看一段代码

1
2
3
4
setTimeout(function(){console.log(1);}, 0);
console.log(2);
// 输出,但是后者次序依旧在上
// 2,1,

console.log(2)在主线程中,先执行,

setTimeout(function(){console.log(1);}, 0)放在了任务队列中,只有在主线程执行完了才会去执行任务列队中的内容

只有主线程的任务执行完成才会执行

事件循环

为什么主线程的任务执行完了后需要不断的扫描任务列队中的内容呢?

看这段代码,有助于你的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log(myData);
ajax()
setTimeout(() => {
console.log('定时器');
}, 2000);
console.log(myData);
const btn = document.querySelector('button')
btn.onclick = () => {
console.log("点击了");
}

// 输出结果如下
// null
// null
// (10次输出)点击了
// ajax返回成功
// {success: true, data: {…}}
// 定时器
// 点击了

我们为button按钮添加了点击事件,在浏览器刷新的同时不停地对按钮进行点击操作(当然是手动点击)

这样是不是可以理解为什么主线程要去循环扫描任务列队了?

事件循环的每一轮称为一个tick(有没有联想到vue中的nextTick?)

当产生用户交互(鼠标点击事件,页面滚动事件,窗口大小变化事件等等),ajax,定时器,计时器等,会向事件循环中的任务队列添加事件,然后等待执行

任务队列

在查找相关文档的时候,我发现有的笔者混淆了任务队列与消息队列,这两个名词虽然相近,但本质上并非是可以混为一谈的事儿,这次我们本次不做深入讨论。

任务队列是类似队列的数据结构,遵循先入先出(FIFO)的规则。

1
2
3
4
5
6
1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
3. 一但"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
4. 主线程不断重复上面的第三步。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复,这种机制就被称为事件循环(event loop)机制。

img

JavaScript 通过 事件循环 event loop 的机制解决了异步时间执行顺序排序的问题。

事件循环 机制和 任务队列 的维护是由事件触发线程控制的。

事件触发线程 同样是浏览器渲染引擎提供的,它会维护一个 任务队列

JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等…),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应挂起的 回调函数 重新加入到任务队列中,消息队列中的回调函数等待被执行。

同时,JS引擎线程会维护一个 执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。

如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从任务队列取出一个任务(即异步的回调函数)放入执行栈中执行。

这里关于线程的进程的说法比较模糊,后续我会单开一文着重复述一下线程进程

前端异步场景分析

  1. 定时任务:setTimeout,setInverval
  2. 网络请求:ajax请求,img图片的动态加载
  3. ES6中的Promise
  4. 事件绑定或者叫DOM事件。比如一个点击事件,我不知道它什么时候点,但是在它点击之前,我该干什么还是干什么。用addEventListener注册一个类型的事件的时候,浏览器会有一个单独的模块去接收这个东西,当事件被触发的时候,浏览器的某个模块,会把相应的函数扔到任务队列中,如果现在任务列队中是空的,就会直接执行这个函数。

需要异步的场景分析

到底什么时候需要异步呢?这里我们稍微分析一下平时的业务场景。

  1. 在可能发生等待的情况
  2. 等待过程中不能像alert一样阻塞程序的时候
  3. 因此,所有的“等待的情况”都需要异步

一句话总结就是需要等待但是又不能阻塞程序的时候需要使用异步

异步和并行

千万不要把异步和并行搞混了

异步:单线程的,主线程的任务以同步的方式执行完毕,才会去依次执行任务列队中的异步任务

并行:两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)

结语

主题是JS异步,话题有点歪到JS的执行顺序机制里边了,而且自己似乎还没顺明白,这里有点差了。

温故而知新,今天扫了很多文档才知道,消息队列和任务队列原来是同一回事儿,早些年就没怎么关注这些东西。

参考

一篇搞定(Js异步、事件循环与消息队列、微任务与宏任务)

关于js中的同步和异步

异步和单线程——什么时候需要异步,前端使用异步的场景

Javascript异步编程的4种方法