0%

任务轮询

JavaScript 语言的一大特点就是单线程,也就是说同一个时间只能处理一个任务。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,(事件循环)Event Loop方案营运而生。

JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。

  • 主线程中的任务(同步代码)执行完后,才执行任务队列中的任务(异步代码)
  • 有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务
  • 比如多个 setTimeout 同时到时间了,就要依次执行

任务包括 script(整体代码)、 setTimeout、setInterval、DOM渲染、DOM事件、Promise、XMLHTTPREQUEST等

原理分析

  • 立即执行主线程同步代码
  • 所有主线程同步代码执行完毕后,先轮询异步微任务队列,将其中的微任务依次添加到主线程并执行。
  • 微任务队列为空,轮询异步宏任务队列,将其中的异步宏任务依次添加到主线程并执行
  • 所有的任务都是在主线程中执行的

下面通过一个例子来详细分析宏任务与微任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log("Ashuntefannao");
setTimeout(() => {
console.log("setTimeout");
});
new Promise((resolve, reject) => {
console.log("Promise中的同步代码");
resolve();
})
.then((_) => {
console.log("Promise.then1");
})
.then((_) => {
console.log("Promise.then2");
});
console.log("阿顺特烦恼");

# 执行结果
Ashuntefannao
Promise中的同步代码
阿顺特烦恼
Promise.then1
Promise.then2
setTimeout
  1. 立即执行最前面的主线程同步代码,打印结果
  2. 执行setTimeout,将其放入异步宏任务队列
  3. 立即执行后续同步代码,在Promise中
  4. 执行到第一个then,将其放入到异步微任务队列中
  5. 执行到第二个then,将其放入到异步微任务队列中
  6. 立即执行末尾的主线程同步代码,打印结果
  7. 所有主线程同步代码执行完毕,在微任务队列中取出第一个then代码块,放入主线程并执行
  8. 主线程同步代码执行完毕,继续轮询微任务队列,取出第二个then代码块并执行。
  9. 主线程代码执行完毕,微任务队列为空,轮询异步宏任务队列
  10. 取出setTimeout中的代码块,放入主线程,并执行
  11. 主线程同步代码执行完毕,微任务队列为空,异步宏任务队列为空
EventLoop_1

脚本加载

引擎在执行任务时不会进行DOM渲染,所以如果把script 定义在前面,要先执行完任务后再渲染DOM。

解决:

  1. script 放在 BODY 结束标签前。
  2. script标签添加type="module",也会延迟解析执行
  3. script标签添加defer="defer"属性
  4. script标签添加async="async"属性
  • defer/async属性,只对外部脚本引入生效,多次使用该属性引入其它外部脚本,不能够保证先后顺序,若引入的多个脚本之间存在依赖关系,需要注意。

定时器

定时器会放入异步宏任务队列,需要等待同步任务、异步微任务执行完成后执行。

下面设置了 6 毫秒执行,如果主线程代码执行10毫秒,定时器要等主线程执行完才执行。

HTML标准规定最小时间不能低于4毫秒,有些异步操作如DOM操作最低是16毫秒,总之把时间设置大些对性能更好。

1
setTimeout(func,6);

下面的代码会先输出 Ashuntefannao 之后输出 阿顺特烦恼

1
2
3
4
setTimeout(() => {
console.log("阿顺特烦恼");
}, 0);
console.log("Ashuntefannao");

微任务

微任务一般由用户代码产生,微任务较宏任务执行优先级更高,Promise.then 是典型的微任务,实例化 Promise 时执行的代码是同步的,then注册的回调函数是异步微任务。

任务的执行顺序是同步任务、微任务、宏任务所以下面执行结果是 1、2、3、4

1
2
3
4
5
6
7
8
9
10
setTimeout(() => console.log(4));

new Promise(resolve => {
resolve();
console.log(1);
}).then(_ => {
console.log(3);
});

console.log(2);

我们再来看下面稍复杂的任务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setTimeout(() => {
console.log("定时器");
setTimeout(() => {
console.log("timeout timeout");
}, 0);
new Promise(resolve => {
console.log("settimeout Promise");
resolve();
}).then(() => {
console.log("settimeout then");
});
}, 0);
new Promise(resolve => {
console.log("Promise");
resolve();
}).then(() => {
console.log("then");
});
console.log("阿顺特烦恼");

以上代码执行结果为

1
2
3
4
5
6
7
Promise
阿顺特烦恼
then
定时器
settimeout Promise
settimeout then
timeout timeout

实例操作

进度条

下面的定时器虽然都定时了一秒钟,但任务队列是按先进先出(先进先执行)原则,依次执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let i = 0;
setTimeout(() => {
console.log(++i);
console.log("timeout1");
}, 1000);

setTimeout(() => {
console.log(++i);
console.log("timeout2");
}, 1000);

//一秒后打印结果
1
timeout1
2
timeout2

下面是一个进度条的示例,将每个数字放在一个任务中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<style>
body {
padding: 30px;
}
.loading {
height: 30px;
background: yellowgreen;
width: 0;
text-align: center;
font-weight: bold;
}
</style>
<body>
<div class="loading"></div>
</body>

<script>
let laodBox=document.querySelector(".loading");
function view() {
let i = 0;
(function handle() {
laodBox.innerHTML = i + "%";
laodBox.style.width = i + "%";
if (i++ < 100) {
setTimeout(handle, 20);
}
})();
}
view();
console.log("定时器开始了...");
</script>

任务分解

一个比较耗时的任务可能造成游览器卡死现象,所以可以将任务拆分为多个异步小任务执行,暂时置于异步任务队列中,当主线程空闲时,在进行任务轮询。下面是一个数字统计的函数,我们会发现运行时间特别长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
console.time("runtime");
function sub(num) {
let count = 0;
for (let i = 0; i <= num; i++) {
count += i;
}
console.log(count);
console.timeEnd("runtime");
}
let num = 987654321;
sub(num);
console.log("阿顺特烦恼"); //需要等待上面执行完才会执行
}

现在把任务分解成小块放入任务队列,游览器就不会出现卡死的现象了,也不会影响后续代码的执行

  • 执行run时,遇到第一个setTimeout,先将其添加到异步宏任务队列。
  • 后面遇到同步代码,打印结果,又遇到异步宏任务,添加到任务队列中
  • 同步代码执行完毕,取出第一个setTimeout到主线程并执行(运算for循环)
  • 后面遇到同步代码,打印结果,又遇到异步宏任务,添加到任务队列中
  • 同步代码执行完毕,取出第二个setTimeout到主线程并执行(调用run)
  • 以此往复,任务轮询……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
let count = 0;
let num = 987654321;
function run() {
setTimeout(() => {
for (let i = 0; i < 100000000; i++) {
if (num <= 0) break;
count += num--;
}
});

if (num > 0) {
console.log(num);
setTimeout(run);
} else {
console.log(num);
console.log(count);
}
}
run();
console.log("阿顺特烦恼");
}

交给微任务处理是更好的选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
async function run(num) {
let res = await Promise.resolve().then((_) => {
let count = 0;
for (let i = 0; i < num; i++) {
count += num--;
}
return count;
});
console.log(res);
}
run(987654321);
console.log("阿顺特烦恼");
}