单线程
JavaScript 语言和执行环境是单线程。即同一时间,只能处理一个任务。
所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推
具体来说,所谓单线程,是指 JS 引擎中负责解释和执行 JavaScript 代码的线程只有一个,也就是一次只能完成一项任务,这个任务执行完后才能执行下一个。所有的任务都需要排队。
JS 为何要被设计为单线程呢?原因如下:
- 首先是历史原因,在最初设计 JS 这门语言时,多进程、多线程的架构并不流行,硬件支持并不好。
- 其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。
- 而且,如果多个线程同时操作同一个 DOM,在多线程不加锁的情况下,会产生冲突,最终会导致 DOM 渲染的结果不符预期。
所以,为了避免这些复杂问题的出现,JS 被设计成了单线程语言。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
同步任务和异步任务
定义
如果当前正在执行的任务执行完成前,它就会阻塞其他正在排队的任务。为了解决这个问题,JS 在设计之初,将任务分成了两类:同步任务、异步任务。
- 同步任务:在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行下一个任务。
- 异步任务:不进入主线程、而是进入任务队列(Event Queue)的任务,该任务不会阻塞后面的任务执行。只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
代码举例:
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.js 中的一些方法回调。
- 3、ES6 中的 Promise、Generator、async/await
Promises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:
f1().then(f2);
现在的大部分软件项目,都是前后端分离的。后端生成接口,前端请求接口。前端发送 ajax 请求,向后端请求数据,然后等待一段时间后,才能拿到数据。这个请求过程就是异步任务。
接口调用的方式
js 中常见的接口调用方式,有以下几种:
- 原生 ajax、基于 jQuery 的 ajax
- Promise
- Fetch
- axios
下一篇文章,我们重点讲一下接口调用里的 Ajax,然后在 ES6 语法中学习 Promise。在这之前,我们需要先了解同步任务、异步任务的事件循环机制。
事件循环机制(重要)
执行顺序如下:
- 同步任务:进入主线程后,立即执行。
- 异步任务:会先进入 Event Table;等时间到了之后,再进入 Event Queue,然后排队(为什么要排队?因为同一时间,JS 只能执行一个任务)。比如说,
setTimeout(()=> {}, 1000)
这种定时器任务,需要等一秒之后再进入 Event Queue。 - 当主线程的任务执行完毕之后,此时主线程处于空闲状态,于是会去读取 Event Queue 中的任务队列,如果有任务,则进入到主线程去执行。
多次异步调用的顺序
- 多次异步调用的结果,顺序可能不同步。
- 异步调用的结果如果存在依赖,则需要通过回调函数进行嵌套。
异步任务举例
例 1:加载图片
// 加载图片的异步任务
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 元素
// 函数封装:定义一个定时器,每间隔 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);