Web/06-JavaScript基础:异步编程/01-单线程和异步任务.md

253 lines
9.1 KiB
Markdown
Raw Normal View History

---
2023-06-08 16:38:04 +08:00
title: 01-单线程和异步任务
---
<ArticleTopAd></ArticleTopAd>
2021-05-17 09:56:06 +08:00
## 单线程
2023-06-08 16:38:04 +08:00
### JS 是单线程的
JavaScript 语言的执行是**单线程**的。即同一时间,只能处理一个任务。
2021-05-17 09:56:06 +08:00
2023-06-08 16:38:04 +08:00
具体来说,所谓单线程,是指 JS 引擎中负责解释和执行 JavaScript 代码的线程只有一个,即同一时间,只能处理一个任务。这个任务执行完后才能执行下一个。所有的任务都**需要排队**。
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
**JS 为何要被设计为单线程呢**?原因如下:
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
- 首先是历史原因,在最初设计 JS 这门语言时,多进程、多线程的架构并不流行,硬件支持并不好。
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
- 其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
- 而且,如果多个线程同时操作同一个 DOM在多线程不加锁的情况下会产生冲突最终会导致 DOM 渲染的结果不符预期。
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
所以为了避免这些复杂问题的出现JS 被设计成了单线程语言。
2021-05-17 09:56:06 +08:00
2023-06-08 16:38:04 +08:00
### 浏览器是多进程、多线程的
JS代码在执行时有它的运行环境也称之为“容器”这个运行环境可以是浏览器也可以是 Node.js 环境。
浏览器是多进程的,**每打开一个新的 tab 标签页就会开启一个新的进程**。每个进程之间是独立的,这是为了防止一个页面卡死而造成所有页面都无法响应,甚至整个浏览器强制退出。
**每个进程中有很多个线程**其中有一个专门执行JS代码的线程所以我们常说JS是单线程的这没有说错。从JS语言的角度看我们把这个线程称为“**主线程**”。
如果JS正在执行某个耗时的任务则当前的线程会被阻塞那应该怎么办呢
实际上,**耗时的任务并不是在主线程中执行的**。因为浏览器的当前进程中有很多个线程,我们可以把耗时任务交给浏览器的其它线程来协助处理,然后在特定的时机通知主线程,该任务则会进入主线程同步完成。
比如现在有一个三秒延迟的定时器任务。计时工作是交给浏览器的其他线程完成的等三秒时间到了之后通知JS主线程该任务进入主线程进行同步执行。
2021-05-17 09:56:06 +08:00
## 同步任务和异步任务
### 定义
如果当前正在执行的任务执行完成前,它就会**阻塞**其他正在排队的任务。为了解决这个问题JS 在设计之初,将任务分成了两类:同步任务、异步任务。
2021-05-17 16:58:16 +08:00
- 同步任务:在**主线程**上排队执行的任务。只有前一个任务执行完毕,才能执行下一个任务。
- 异步任务:不进入主线程、而是进入**任务队列**Event Queue的任务该任务不会阻塞后面的任务执行。只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
2021-05-17 16:58:16 +08:00
2021-05-27 12:54:01 +08:00
代码举例:
2021-05-17 16:58:16 +08:00
```js
2021-05-27 12:54:01 +08:00
console.log('同步任务1');
2021-05-17 16:58:16 +08:00
setTimeout(() => {
console.log('异步任务');
}, 1000);
2021-05-27 12:54:01 +08:00
console.log('同步任务2');
2021-05-17 16:58:16 +08:00
```
2021-05-27 12:54:01 +08:00
打印结果是:
```
同步任务1
同步任务2
异步任务
```
2021-05-17 09:56:06 +08:00
2021-05-27 12:54:01 +08:00
代码解释:第一行代码是同步任务,会**立即执行**;定时器里的回调函数是异步任务,需要等 1 秒后才会执行。假如定时器里的代码是同步任务那需要等待1秒后才能执行最后一行代码`console.log('同步任务2')`,也就是造成了主线程里的同步任务阻塞,这不是我们希望看到的。
2021-05-26 20:30:35 +08:00
2021-05-27 12:54:01 +08:00
比如说,网络图片的请求,就是一个异步任务。前端如果同时请求多张网络网络图片,谁先请求完成就让谁先显示出来。假如网络图片的请求做成同步任务,那就会出大问题,所有图片都得排队加载,如果第一张图片未加载完成,就得卡在那里,造成阻塞,导致其他图片都加载不出来。页面看上去也会很卡顿,这肯定是不能接受的。
2021-05-26 20:30:35 +08:00
### 前端使用异步编程的场景
2021-05-17 16:58:16 +08:00
2021-05-27 12:54:01 +08:00
什么时候需要**等待**,就什么时候用异步。常见的异步场景如下:
2021-05-17 09:56:06 +08:00
- 1、事件监听比如说按钮绑定点击事件之后用户爱点不点。我们不可能卡在按钮那里什么都不做。所以应该用异步
- 2、回调函数
- 2.1、定时器setTimeout定时炸弹、setInterval循环执行
- 2.2、ajax请求。
- 2.3、Node.js 中的一些方法回调。
- 3、ES6 中的 Promise、Generator、async/await
2021-05-17 09:56:06 +08:00
2021-05-18 21:25:52 +08:00
现在的大部分软件项目,都是前后端分离的。后端生成接口,前端请求接口。前端发送 ajax 请求,向后端请求数据,然后**等待一段时间**后,才能拿到数据。这个请求过程就是异步任务。
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
### 接口调用的方式
js 中常见的接口调用方式,有以下几种:
- 原生 ajax、基于 jQuery 的 ajax
- Promise
- Fetch
- axios
2023-06-08 16:38:04 +08:00
后续文章,我们会重点讲一下接口调用里的 Ajax然后在 ES6 语法中学习 **Promise**。在这之前,我们需要先了解同步任务、异步任务的事件循环机制。
2021-05-17 09:56:06 +08:00
2021-05-26 23:54:42 +08:00
2021-05-27 12:54:01 +08:00
### 多次异步调用的顺序
- 多次异步调用的结果,顺序可能不同步。
- 异步调用的结果如果**存在依赖**,则需要通过回调函数进行嵌套。
2021-05-26 23:54:42 +08:00
## 定时器:代码示例
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
掌握了上面的事件循环原理之后,我们来看几个例子。
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
### 举例 1
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
```js
console.log(1);
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
setTimeout(() => {
console.log(2);
}, 1000);
console.log(3);
console.log(4);
```
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
打印结果:
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
```
1
3
4
2
```
2021-05-17 09:56:06 +08:00
2021-05-17 16:58:16 +08:00
解释:先等同步任务执行完成后,再执行异步任务。
### 举例 2重要
如果我把上面的等待时间,从 1 秒改成 0 秒,你看看打印结果会是什么。
```js
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
console.log(4);
```
打印结果:
```
1
3
4
2
```
可以看到,打印结果没有任何变化,这个题目在面试中经常出现,考的就是 `setTimeout(()=> {}, 0)`会在什么时候执行。这就需要我们了解同步任务、异步任务的执行顺序,即前面讲到的**事件循环机制**。
解释:先等同步任务执行完成后,再执行异步任务。
同理,我们再来看看下面这段伪代码:
```js
setTimeout(() => {
console.log('异步任务');
}, 2000);
// 伪代码
sleep(5000); //表示很耗时的同步任务
```
2021-05-26 23:54:42 +08:00
上面的代码中,异步任务不是 2 秒之后执行,而是等耗时的同步任务执行完毕之后,才执行。那这个异步任务,是在 5 秒后执行?还是在 7 秒后执行?这个作业,留给读者你来思考~
2021-05-17 16:58:16 +08:00
### 举例 3较真系列
```js
setTimeout(() => {
console.log('异步任务');
}, 1000);
```
上面的代码中,等到 1 秒之后,真的会执行异步任务吗?其实不是。
2021-05-27 21:56:03 +08:00
在浏览器中, setTimeout()/ setInterval() 的每调用一次定时器的最小时间间隔是**4毫秒**,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的 setInterval 的回调函数阻塞导致的。
2021-05-26 23:54:42 +08:00
上面的案例中,异步任务需要等待 1004 毫秒之后,才会从 Event Table 进入到 Event Queue。这在面试中也经常被问到。
## 异步任务举例
2021-05-27 12:54:01 +08:00
### 例 1加载图片
2021-05-26 23:54:42 +08:00
```js
// 加载图片的异步任务
function loadImage(file, success, fail) {
const img = new Image();
img.src = file;
img.onload = () => {
// 图片加载成功
success(img);
};
img.onerror = () => {
// 图片加载失败
fail(new Error('img load fail'));
};
}
loadImage(
'images/qia nguyihao.png',
(img) => {
console.log('图片加载成功');
document.body.appendChild(img);
img.style.border = 'solid 2px red';
},
(error) => {
console.log('图片加载失败');
console.log(error);
}
);
```
2021-05-27 12:54:01 +08:00
### 例 2定时器计时移动 DOM 元素
2021-05-26 23:54:42 +08:00
2021-05-27 12:54:01 +08:00
```js
// 函数封装:定义一个定时器,每间隔 delay 毫秒之后,执行 callback 函数
function myInterval(callback, delay = 100) {
let timeId = setInterval(() => callback(timeId), delay);
}
myInterval((timeId) => {
// 每间隔 500毫秒之后向右移动 .box 元素
const myBox = document.getElementsByClassName('box')[0];
const left = parseInt(window.getComputedStyle(myBox).left);
myBox.style.left = left + 20 + 'px';
if (left > 300) {
clearInterval(timeId);
// 每间隔 10 毫秒之后,将 .box 元素的宽度逐渐缩小,直到消失
myInterval((timeId2) => {
const width = parseInt(window.getComputedStyle(myBox).width);
myBox.style.width = width - 1 + 'px';
if (width <= 0) clearInterval(timeId2);
}, 10);
}
}, 200);
```
2021-05-26 23:54:42 +08:00
2021-05-17 09:56:06 +08:00
## 参考链接
2021-05-17 16:58:16 +08:00
- [JS-同步任务,异步任务,微任务,和宏任务](https://github.com/PleaseStartYourPerformance/javaScript/issues/34)
- [JS 同步异步宏任务微任务](https://juejin.cn/post/6875605533127081992)、[JavaScript 中事件循环的理解](https://zhuanlan.zhihu.com/p/364475433)、[javascript 事件循环机制](https://github.com/reng99/blogs/issues/34)
2021-05-18 21:25:52 +08:00
- [如何实现比 setTimeout 快 80 倍的定时器?](https://mp.weixin.qq.com/s/NqzWkeOhqAU85XPkJu_wCA)