文章目录
1. 前言
在 JavaScript 中,任务可以分为同步任务、异步任务,而异步任务又进一步细分为宏任务和微任务
- 同步任务:指那些在主线程上排队执行的任务,只有前一个任务执行完毕后,才能开始执行下一个任务
- 异步任务:异步任务不会进入主线程,而是进入 "任务队列" 中等待执行
JavaScript 语言本身是单线程,这意味着在任何时刻,JavaScript 引擎中只有一条主线程来处理所有的任务
2. 同步任务
同步任务只有前一个任务执行完毕后,才能开始执行下一个任务,特点是按照顺序执行,会阻塞后续代码的执行
javascript
console.log(1);
console.log(2);
3. 异步任务
异步任务是只有当主线程上的同步任务全部执行完毕后,才会从任务队列中取出异步任务进入主线程执行
javascript
console.log(1); // 同步任务
setTimeout(() => console.log(3), 0) // 异步任务,等待同步任务执行完毕之后,才会执行
console.log(2); // 同步任务
宏任务
宏任务是由宿主环境发起的异步任务,通常包含较长的等待时间或需要等待 I/O 操作完成
常见的宏任务:setTimeout / setInterval、ajax 请求、dom 事件
微任务
微任务通常用于处理 Promise 或其他需要尽快执行的操作
常见的微任务:Promise.then、async / await
javascript
setTimeout(() => console.log('定时器 - 宏任务'), 0)
Promise.resolve().then(value => {
console.log('Promise - 微任务');
})
console.log('123 - 同步任务')
// 123 - 同步任务
// Promise - 微任务
// 定时器 - 宏任务
4. 定时器的任务编排
在 JavaScript 中,setTimeout 函数的延时时间的最小值通常是 4 毫秒
这是因为在某些浏览器或操作系统中,为了优化性能和节能,定时器的实际执行可能会有一定的最小延迟
javascript
// 即使设置了非常小的延时,浏览器也可能不会立即执行定时器回调函数,而是等待至少 4 毫秒后才执行
setTimeout(() => console.log('定时器'), 0)
设置定时器延时 200 毫秒执行,那么它真的就会 200 毫秒后立即执行吗 ?通过以下代码可以发现并不是这样的
代码解析原因:因为定时器是异步任务,for 循环是同步任务,所以定时器要等 for 循环执行完之后才会执行定时器
定时器的任务编排原理:到达延时时间会先将其放到宏任务队列中,等待主线程处理完同步任务,再来处理异步任务
javascript
console.log(new Date().getTime() / 1000);
setTimeout(() => {
console.log('定时器:' + new Date().getTime() / 1000);
}, 200)
console.log('同步任务')
for (let i = 0; i < 20000; i++) {
console.log('');
}
5. Promise 微任务处理逻辑
Promise 是微任务中的典型代表,Promise 的构造函数代码是立即执行的,也就是说它是同步代码
javascript
// 4. 定时器,放入宏任务队列
setTimeout(() => console.log('定时器'))
new Promise(resolve => {
// 1. 这里是 promise 的构造函数,它是同步代码
console.log('promise');
resolve()
}).then(() => {
// 3. 这里的代码会放到微任务队列
console.log('then');
})
// 2. 同步代码
console.log('hello');
// 由于任务执行顺序是:同步任务 > 微任务 > 宏任务,所以上面代码的输出顺序为
// promise
// hello
// then
// 定时器
如果在定时器内部再放入 Promise 和 定时器,那么执行顺序是怎么样的 ?
javascript
setTimeout(() => {
// 1. 同步代码
console.log('定时器')
// 因为它是在定时器内部,所以它在宏任务执行之后才会放入任务队列,才会执行
new Promise(resolve => {
// 2. 同步代码
console.log('timeout promise');
resolve()
}).then(() => {
// 3. 微任务
console.log('timeout then');
})
// 4. 宏任务
setTimeout(() => console.log('定时器内部的定时器'))
})
new Promise(resolve => {
console.log('promise');
resolve()
}).then(() => {
console.log('then');
})
console.log('hello');
// 其实很简单,在上一段代码的结果上,继续按照任务执行优先顺序执行
// promise
// hello
// then
// 定时器
// timeout promise
// timeout then
// 定时器内部的定时器
6. DOM 渲染任务
浏览器在解析页面时,也是按照顺序从上往下解析的,如下所示,先解析 utils.js,然后再渲染 h1 标签
也可以理解为先将解析 utils.js 的任务执行完,再执行渲染 h1 标签的任务
html
<head>
<script src="./utils.js"></script>
</head>
<body>
<h1>JavaScript 的宏任务和微任务</h1>
</body>
如果执行 utils.js 的时间比较长,那么页面将迟迟不渲染,进入页面会有较长时间的空白内容
javascript
for (let index = 0; index < 5000000; index++) {
document.write(' ')
}
我们可以将 utils.js 的引入放到内容渲染后面,改变任务的执行顺序,进入页面就不会有长时间的空白了
html
<body>
<h1>JavaScript 的宏任务和微任务</h1>
<script src="./utils.js"></script>
</body>
7. 任务共享内存
任务共享内存是指多个任务可以访问同一块内存区域
多个定时器设置相同时间延时执行,它们并不会同时执行,而是到达设定的时间后将它们放入任务队列,依次执行任务
javascript
let i = 0
setTimeout(() => {
// 宏任务
console.log(++i);
}, 1000)
setTimeout(() => {
// 宏任务
console.log(++i);
}, 1000)
8. 进度条实例体验任务轮询
先写好一个进度条的样式
html
<head>
<style>
.container {
width: 200px;
border-radius: 3px;
border: 1px solid #ccc;
}
#progress {
width: 0%;
color: #fff;
text-align: center;
background-color: #376de1;
}
</style>
</head>
<body>
<div class="container">
<div id="progress">0</div>
</div>
</body>
定时器其实就是往任务队列中不断的放入任务,到达执行时间后,交由主线程依次执行任务
javascript
function handle() {
let i = 0;
(function run() {
if (++i <= 100) {
progress.innerHTML = i
progress.style.width = i + '%'
setTimeout(run, 50)
}
})();
}
handle()
9. 任务拆分为多个子任务
现有以下代码,前面的累加函数需要较长时间才能执行完成,阻塞了后面代码的运行,我们来看一下怎么解决这个问题
javascript
let count = 0
let num = 987654321
function task() {
for (let i = 0; i < num; i++) {
count += num--
}
console.log(count);
}
task() // 同步任务
console.log('hello'); // 同步任务,需要等待 task() 执行完后才能执行
思路是将大量的计算放入异步任务,让同步任务先执行,以免造成阻塞
javascript
let count = 0
let num = 10000000
function task() {
// 首次调用先累加一次
for (let i = 0; i <= num; i++) {
if (num <= 0) break;
count += num--
}
if (num > 0) {
// 后续累加,放入宏任务执行
setTimeout(task)
} else {
console.log(count)
}
}
task()
console.log('hello')
10. Promise 微任务处理复杂逻辑
将定时器放入 Promise 也可以处理复杂逻辑,不影响同步任务的执行,但下面用的还是宏任务(定时器)
javascript
let count = 0
let num = 987654321
// 由于 promise 里面的代码是同步执行的,在里面直接写循环还是会卡住,所以可以写个定时器
function task() {
return new Promise(resolve => {
let count = 0
setTimeout(() => {
for (let i = 0; i < num; i++) {
count += num--
}
resolve(count)
})
});
}
async function getSum(num) {
let res = await task(num)
console.log(res);
}
getSum(num)
console.log('hello');
因为 Promise 的 then 回调方法是微任务,那么我们可以将代码调整为:
javascript
let num = 987654321
async function getSum(num) {
let res = await Promise.resolve().then(_ => {
let count = 0
for (let i = 0; i < num; i++) {
count += num--
}
return count
})
}
getSum(num)
console.log('hello');