前端为何会有异步这一说法,是因为前端的代码是在浏览器中运行的,而浏览器是单线程的,前端的代码是在主线程中运行的,如果有耗时的操作,就会阻塞主线程,导致页面卡顿,所以就有了异步这一说法。
1. 什么是异步
异步是指程序的执行顺序与代码的书写顺序不一致,这里的异步是指异步任务,异步任务是指不会阻塞主线程的任务,比如 setTimeout
、Promise
、Ajax
等。
在 JavaScript
中,异步任务是通过回调函数来实现的,回调函数是指在异步任务执行完毕后,会调用的函数。
2. 异步的实现
2.1 回调函数
回调函数是指在异步任务执行完毕后,会调用的函数,回调函数是异步任务的一种实现方式。
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
// 1
// 3
// 2
上面的代码中,setTimeout
是异步任务,所以会在主线程执行完毕后,才会执行,所以会先打印 1,然后打印 3,最后打印 2。
2.2 Promise
Promise
是异步任务的另一种实现方式。
console.log(1);
Promise.resolve()
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
});
console.log(4);
// 1
// 4
// 2
// 3
上面的代码中,Promise.then
里面的回调函数就是异步方法,所以会先打印 1,然后打印 4,最后打印 2,3。
2.3 async/await
async/await
是异步任务的另一种实现方式,本质上是类似于 Promise
的实现方式。
console.log(1);
async function async1() {
await async2();
console.log(2);
}
async function async2() {
console.log(3);
}
async1();
console.log(4);
// 1
// 3
// 4
// 2
上面的代码中,async1
函数中的 await async2()
是异步任务,所以会先打印 1,然后打印 3,最后打印 4,2。
3. 异步的优缺点
3.1 优点
- 通过异步任务,可以避免阻塞主线程,提高代码的执行效率。
- 通过异步任务,可以实现多个任务的并行执行,提高代码的执行效率。
3.2 缺点
- 通过异步任务,无法获取异步任务的返回值,只能通过回调函数来获取异步任务的返回值。
- 需要通过回调函数来实现异步任务,回调函数的嵌套层级会很深,导致代码的可读性很差。
- 需要解决异步同步的问题,比如在异步任务中,如果需要使用同步任务,就可能需要使用多个回调函数来实现。
4. 异步的解决方案
4.1 Promise
Promise
是异步任务的另一种实现方式,它的优点是可以解决回调函数的嵌套问题,可以通过链式调用的方式来实现异步任务。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
promise
.then((res) => {
console.log(res);
return 2;
})
.then((res) => {
console.log(res);
return 3;
})
.then((res) => {
console.log(res);
});
// 1
// 2
// 3
上面的代码中,promise.then
里面的回调函数就是异步任务,所以会先打印 1,然后打印 2,最后打印 3。
这里只讲异步,不讲Promise
的其他用法,包括后面的也只是简单介绍一下使用,因为每一个都可以单独写一个章节,所以这里只需要有一个印象就可以了,感兴趣的可以去看阮一峰老师的《ES6 入门》。
4.2 async/await
async/await
是异步任务的另一种实现方式,本质上是类似于 Promise
的实现方式。
async function async1() {
console.log(1);
const res = await async2();
}
async function async2() {
console.log(2);
}
async1().then(() => {
console.log(3);
});
console.log(4);
// 1
// 2
// 4
// 3
上面的代码如果转换成 Promise
的实现方式,就是下面这样。
function async1() {
console.log(1);
return async2().then((res) => {
console.log(3);
});
}
function async2() {
console.log(2);
return Promise.resolve();
}
async1();
console.log(4);
setTimeout
setTimeout
是老牌异步任务的标杆了,他的优点就是兼容性好,缺点就是不够精确。
setTimeout(() => {
console.log(1);
}, 1000);
console.log(2);
// 2
// 1
上面的代码中,setTimeout
是异步任务,所以会先打印 2,然后打印 1。
setInterval
setInterval
和setTimeout
的用法基本一样,只是setInterval
是每隔一段时间就执行一次。
setInterval(() => {
console.log(1);
}, 1000);
console.log(2);
// 2
// 1
// 1
// 1
// ...
setInterval
可能会造成内存泄漏,为啥会造成内存泄露一般是因为开发者在不需要使用setInterval
的时候,没有及时清除setInterval
,导致setInterval
一直在执行,所以会造成内存泄漏。
requestAnimationFrame
requestAnimationFrame
是浏览器提供的一个 API,可以实现动画的流畅性,不同于setTimeout
和setInterval
,requestAnimationFrame
是根据浏览器的刷新频率来执行的。
function animation() {
console.log(1);
requestAnimationFrame(animation);
}
animation();
console.log(2);
// 2
// 1
// 1
// 1 大约每次间隔 16ms
MessageChannel
MessageChannel
可以创建一个消息通道,并且可以通过提供的两个 MessagePort
发送消息。
const channel = new MessageChannel();
channel.port1.onmessage = () => {
console.log(1);
};
channel.port2.postMessage(1);
console.log(2);
// 2
// 1
MessageChannel
的使用场景通常是伴随着Ifame
的使用,比如在Iframe
中使用MessageChannel
来实现跨域通信。
MutationObserver
MutationObserver
可以监听 DOM 的变化,当 DOM 发生变化时,就会触发回调函数。
const observer = new MutationObserver(() => {
console.log(1);
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
document.body.appendChild(document.createElement("div"));
console.log(2);
// 2
// 1
MutationObserver
的使用场景通常是监听 DOM 的变化,比如监听input
的变化,实时获取输入的值。
Web Worker
Web Worker
是 HTML5 中新增的一个 API,它可以实现多线程的功能,可以在后台运行一个脚本,不会阻塞主线程。
const worker = new Worker("worker.js");
worker.onmessage = (e) => {
console.log(e.data);
};
worker.postMessage(1);
console.log(2);
// 2
// 1
Web Worker
的使用场景通常是用来做一些比较耗时的任务,比如图片压缩、视频转码等。
Service Worker
Service Worker
是 HTML5 中新增的一个 API,它可以实现离线缓存,可以拦截请求,可以实现消息推送等功能。
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").then(() => {
console.log("注册成功");
});
}
console.log(2);
// 2
// 注册成功
Service Worker
的使用场景通常是用来做离线缓存,比如在移动端开发中,可以将一些静态资源缓存到本地,当用户断网时,可以从本地缓存中读取资源。
总结
本文主要介绍了 JavaScript 中的宏任务和微任务,以及宏任务和微任务的执行顺序,以及 JavaScript 中的异步任务的实现原理,以及异步任务的使用场景。
当然现在使用宏任务和微任务描述事件循环已经不太合适了,因为宏任务和微任务的概念已经被废弃了,现在的事件循环已经不再区分宏任务和微任务,而是将所有的任务都放到任务队列中,然后按照先进先出的顺序依次执行。
任务队列再分其他的任务队列,比如微任务队列、宏任务队列、I/O 任务队列、UI 渲染任务队列等,每个任务队列都有自己的执行顺序,比如微任务队列中的任务都是在当前任务执行完之后立即执行,而宏任务队列中的任务都是在当前任务执行完之后,下一个事件循环才会执行。