Web/06-JavaScript基础:异步编程/01-单线程和异步任务.md
2023-06-24 11:21:25 +08:00

253 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 01-单线程和异步任务
---
<ArticleTopAd></ArticleTopAd>
## 单线程
### JS 是单线程的
JavaScript 语言的执行是**单线程**的。即同一时间,只能处理一个任务。
具体来说,所谓单线程,是指 JS 引擎中负责解释和执行 JavaScript 代码的线程只有一个,即同一时间,只能处理一个任务。这个任务执行完后才能执行下一个。所有的任务都**需要排队**。
**JS 为何要被设计为单线程呢**?原因如下:
- 首先是历史原因,在最初设计 JS 这门语言时,多进程、多线程的架构并不流行,硬件支持并不好。
- 其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。
- 而且,如果多个线程同时操作同一个 DOM在多线程不加锁的情况下会产生冲突最终会导致 DOM 渲染的结果不符预期。
所以为了避免这些复杂问题的出现JS 被设计成了单线程语言。
### 浏览器是多进程、多线程的
JS代码在执行时有它的运行环境也称之为“容器”这个运行环境可以是浏览器也可以是 Node.js 环境。
浏览器是多进程的,**每打开一个新的 tab 标签页就会开启一个新的进程**。每个进程之间是独立的,这是为了防止一个页面卡死而造成所有页面都无法响应,甚至整个浏览器强制退出。
**每个进程中有很多个线程**其中有一个专门执行JS代码的线程所以我们常说JS是单线程的这没有说错。从JS语言的角度看我们把这个线程称为“**主线程**”。
如果JS正在执行某个耗时的任务则当前的线程会被阻塞那应该怎么办呢
实际上,**耗时的任务并不是在主线程中执行的**。因为浏览器的当前进程中有很多个线程,我们可以把耗时任务交给浏览器的其它线程来协助处理,然后在特定的时机通知主线程,该任务则会进入主线程同步完成。
比如现在有一个三秒延迟的定时器任务。计时工作是交给浏览器的其他线程完成的等三秒时间到了之后通知JS主线程该任务进入主线程进行同步执行。
## 同步任务和异步任务
### 定义
当前正在执行的任务,如果没有执行完成,它可能会**阻塞**其他正在排队的任务。为了解决这个问题JS 在设计之初,将任务分成了两类:同步任务、异步任务。
- 同步任务:在**主线程**上排队执行的任务。只有当前任务执行完毕,才能执行下一个任务。当前任务在没有得到结果之前,不会继续后续操作。
- 异步任务:不进入主线程、而是进入**任务队列**Event Queue的任务该任务无论有没有得到结果都不会阻塞后续任务的执行。只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
代码举例:
```js
console.log('同步任务1');
setTimeout(() => {
console.log('异步任务');
}, 1000);
console.log('同步任务2');
```
打印结果是:
```
同步任务1
同步任务2
异步任务
```
代码解释:第一行代码是同步任务,会**立即执行**;定时器里的回调函数是异步任务,需要等 1 秒后才会执行。假如定时器里的代码是同步任务那需要等待1秒后才能执行最后一行代码`console.log('同步任务2')`,也就是造成了主线程里的同步任务阻塞,这不是我们希望看到的。
比如说,网络图片的请求,就是一个异步任务。前端如果同时请求多张网络网络图片,谁先请求完成就让谁先显示出来。假如网络图片的请求做成同步任务,那就会出大问题,所有图片都得排队加载,如果第一张图片未加载完成,就得卡在那里,造成阻塞,导致其他图片都加载不出来。页面看上去也会很卡顿,这肯定是不能接受的。
### 前端使用异步编程的场景
什么时候需要**等待**,就什么时候用异步。常见的异步场景如下:
- 1、事件监听比如说按钮绑定点击事件之后用户爱点不点。我们不可能卡在按钮那里什么都不做。所以应该用异步
- 2、回调函数
- 2.1、定时器setTimeout定时炸弹、setInterval循环执行
- 2.2、ajax请求。
- 2.3、Node.jsFS文件读写、数据库操作。
- 3、ES6 中的 Promise、Generator、async/await
现在的大部分软件项目,都是前后端分离的。后端生成接口,前端请求接口。前端发送 ajax 请求,向后端请求数据,然后**等待一段时间**后,才能拿到数据。这个请求过程就是异步任务。
### 接口调用的方式
js 中常见的接口调用方式,有以下几种:
- 原生 ajax、基于 jQuery 的 ajax
- Promise
- Fetch
- axios
后续文章,我们会重点讲一下接口调用里的 Ajax然后在 ES6 语法中学习 **Promise**。在这之前,我们需要先了解同步任务、异步任务的事件循环机制。
### 多次异步调用的顺序
- 多次异步调用的结果,顺序可能不同步。
- 异步调用的结果如果**存在依赖**,则需要通过回调函数进行嵌套。
## 定时器:代码示例
掌握了上面的事件循环原理之后,我们来看几个例子。
### 举例 1
```js
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
console.log(3);
console.log(4);
```
打印结果:
```
1
3
4
2
```
解释:先等同步任务执行完成后,再执行异步任务。
### 举例 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); //表示很耗时的同步任务
```
上面的代码中,异步任务不是 2 秒之后执行,而是等耗时的同步任务执行完毕之后,才执行。那这个异步任务,是在 5 秒后执行?还是在 7 秒后执行?这个作业,留给读者你来思考~
### 举例 3较真系列
```js
setTimeout(() => {
console.log('异步任务');
}, 1000);
```
上面的代码中,等到 1 秒之后,真的会执行异步任务吗?其实不是。
在浏览器中, setTimeout()/ setInterval() 的每调用一次定时器的最小时间间隔是**4毫秒**,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的 setInterval 的回调函数阻塞导致的。
上面的案例中,异步任务需要等待 1004 毫秒之后,才会从 Event Table 进入到 Event Queue。这在面试中也经常被问到。
## 异步任务举例
### 例 1加载图片
```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);
}
);
```
### 例 2定时器计时移动 DOM 元素
```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);
```
## 参考链接
- [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)
- [如何实现比 setTimeout 快 80 倍的定时器?](https://mp.weixin.qq.com/s/NqzWkeOhqAU85XPkJu_wCA)