两个异步 API
文章开始前,先了解两个经典的异步 API 吧
SetTimeout
这是一个计时器,指定一段时间之后,执行异步执行某个函数
基本语法:
javascript
// 基础语法
setTimeout(callback, delay, param1, param2, ...);
基本使用:
javascript
// 简单示例
setTimeout(() => {
console.log('3秒后执行');
}, 3000);
// 带参数的示例
setTimeout((name, age) => {
console.log(`${name}今年${age}岁`);
}, 1000, '小明', 18);
// 清除定时器
const timer = setTimeout(() => {}, 1000);
clearTimeout(timer);
异步特征
javascript
// 执行顺序示例
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
// 输出顺序:1, 3, 2
- setTimeout 是异步操作
- 不能保证精确的执行时间
使用场景
- 延迟执行某个函数
- 实现异步操作
SetInterval
这也是一个计时器,
- 指定一段时间之后,执行异步执行某个函数
- 循环执行第一个逻辑,直到计时器被清除
基本语法:
javascript
// 基础语法
setInterval(callback, delay, param1, param2, ...);
基本使用:
javascript
// 简单示例
setInterval(() => {
console.log('每2秒执行一次');
}, 2000);
// 清除定时器
const intervalTimer = setInterval(() => {}, 1000);
clearInterval(intervalTimer);
使用场景
- 周期性执行某个任务
- 实现倒计时
- 定期更新数据
- 轮询操作
异步执行是JS编程中特有的表现,好像有两个线程执行代码一样,一个同步线程执行代码,一个异步线程执行异步代码。真的如此吗?
JS内部异步执行逻辑是什么样的呢?下面一起来看看吧
任务队列
浏览器多进程架构
讲任务队列之前,需要先了解浏览器的架构。浏览器的架构是多进程架构,目前有
- 浏览器进程
- 渲染进程
- GPU 进程
- 网络进程
- 插件进程
浏览器进程主要负责进程的管理,页面的显示,用户的交互,文件存储等功能;
渲染进程主要负责解析 html,js,css 文件,并且 javaScript 引擎 V8 也是运行在渲染进程中。
GPU 进程主要是用来绘制页面 UI 的,以及实现复杂的 3D 效果;
网络进程主要负责页面的网络资源的加载;
插件进程就是负责插件的运行的;
在上面的几个进程中,网络进程和插件进程最好理解了。其他三个进程的任务部分中有交叉,主要体现在页面渲染的部分。这三个进程好像多多少少都和 UI 靠点边
解释一下:渲染进程 负责解析 html,css 等文件,其负责页面元素的样式计算,元素的尺寸,颜色,位置等数据,然后将这些数据交给 CPU 进程 进行绘制,绘制成一张张图片,绘制完成后交由给浏览器进程进行显示。
进程之间的通信方式
进程之间如何通信的呢?具体比较复杂,不过我们可以简化一下:进程之间靠消息队列进行
当页面发生点击,滚动等交互事件,浏览器进程会通知渲染进程执行对应的 JS 逻辑;当网络资源加载完成,通知渲染进程接口响应结束了,快来处理等等。
其实,渲染进程收到这些消息并不会马上处理,而是将这些消息生成对应的任务,放在一个任务队列里面,然后依次从任务队列中取出任务来执行。
什么是队列呢?队列是一种先进先出的数据结构。从队尾入队,从队头出队,先进来的先出去,就像排队一样,所以这种数据结构叫队列
任务队列
目前浏览器有两种任务:
- 宏任务
- 微任务
对应的,浏览器就有两种队列:
- 宏任务队列(Macrotask Queue):script、event script、setTimeout、setInterval等
- 微任务队列(Microtask Queue):Promise、MutationObserver 等
其中,宏任务一般是 script 脚本,用户交互的事件脚本,比如点击事件、setTimeout 回调函数等;微任务队列一般是promise
它们的执行顺序 是:浏览器会先执行宏任务,然后再执行所有的**微任务;然后是下一个宏任务,然后再是所有的微任务,如此循环
例子
下面看一段代码:
html
<button id="clickme">
点击我
</button>
<script>
const button = document.getElementById("clickme");
button.onclick = ()=>{
console.log('click me');
}
</script>
<script>
console.log("开始"); // 同步任务
setTimeout(() => {
console.log("宏任务:setTimeout"); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log("微任务:Promise"); // 微任务
});
console.log("结束"); // 同步任务
</script>
上面的代码是一段可以直接放在 html 文件中执行的代码:
在浏览器打开这个文件,查看控制台:
然后点击按钮:
结果看到了,我们从任务队列的角度分析下为什么是这样。
这个文件中,有两个 script 脚本,浏览器首先会分别为 script 脚本创建两个宏任务,并且放到任务队列中。做完之后,就开始从任务队列中获取任务开始执行
第一个执行的是第一个 script 脚本中的代码,这里面代码会给 button 添加点击事件。
执行完成后,继续从任务队列中获取下一个宏任务,也就是下一个 script 脚本。
代码从上往下开始执行,先执行:
javascript
console.log("开始");
控制台会打印出开始
然后执行:
javascript
setTimeout(() => {
console.log("宏任务:setTimeout"); // 宏任务
}, 0);
这个代码会将传入的回调函数,生成一个定时任务 ,并将其放在定时任务队列中。
Q: 什么时候定时任务会从队列中取出来呢?
A: 在浏览器每执行完当前的宏任务和所有的微任务,就会去定时任务队列中扫描,看看有没有计时结束的任务,如果有,就将其取出,并放入宏任务队列中。
注意,当前宏任务还没执行完呢,需要继续往下执行!
下面接着执行:
javascript
Promise.resolve().then(() => {
console.log("微任务:Promise"); // 微任务
});
这个 promise 如果你还不是很明白,没关系,你只需要知道,这个代码会生成一个微任务,并会被放入微任务队列中。
每个宏任务都有这样一个微任务队列,在当前宏任务执行过程中,生成的微任务都会放到这个队列中。
实际的内部实现可能不是这个样子,但是这样理解完全没有问题
注意,当前宏任务还没执行完呢,需要继续往下执行!
下面接着执行:
javascript
console.log("结束");
紧接着控制台会打印出结束
好,现在,当前宏任务才算执行完成了。下面开始执行所有的微任务了。
当前微任务队列有一个任务,执行它。控制台就打印出了微任务:Promise
微任务队列清空了。
现在看看有没有到时间了的定时任务啊,有一个!然后将其取出放入宏任务队列中。
好,现在,浏览器的渲染进程继续从任务队列中获取下一个宏任务,这时候取到了之前生成的定时任务,执行它。控制台就打印出了宏任务:setTimeout
。
当前宏任务执行结束,浏览器的渲染进程会继续从任务队列中获取下一个宏任务。任务队列也可能为空,渲染进程就会进入休眠状态,等待下一个任务进入队列中。
这时候,用户点击了页面上的按钮:
浏览器进程会告诉渲染进程该按钮被点击了,渲染进程会生成一个宏任务放入任务队列中。
然后从中取出该任务,并执行它。控制台就打印出了click me
。
好,上面就是打印结果背后,浏览器做的所有事情。
通过这个例子,我们可以从中深刻地感受到什么是宏任务,微任务,定时任务,以及它们之间的执行顺序的逻辑。
小结:
执行顺序:浏览器会先执行宏任务,然后再执行所有的微任务;然后是下一个宏任务,然后再是所有的微任务,如此循环
定时任务: 在浏览器每执行完当前的宏任务和所有的微任务,就会去定时任务队列中扫描,看看有没有计时结束的任务,如果有,就将其取出,并放入宏任务队列中。
微任务的及时性: 新生成的微任务都会在当前宏任务执行结束之后,全部被执行
定时任务的不准确性:不准确性体现在两方面,一只有在当前宏任务和所有微任务执行结束后才会检查定时队伍队列;二,取出定时任务放入宏任务队列中,是放入队尾,而不是队首。
做个题吧
请给出下面的代码的执行结果
javascript
console.log("1");
setTimeout(() => {
console.log("2");
}, 500);
console.log("3");
setTimeout(() => {
console.log("4");
}, 0);
console.log("5");
javascript
console.log("First");
setTimeout(() => {
console.log("Second");
}, 100);
for (let i = 0; i < 10000; i++) {
console.log('...')
}
console.log("Third");
上面的题目大家可以在浏览器跑一跑,看看结果🤭
Second
是在 100ms 后打印出来的吗,为什么?