第十六章 任务队列
宏任务与微任务
说一说宏任务和微任务对浏览器的阻塞关系
microtask
promise.[then/catch/finally]
MutationObserver
macrotask
setTimeout / setInterval
MessageChannel I/O
requestIdleCallback 与macrotask性质相同
requestAnimationFrame
请求动画帧,用于动画。
替代定时器匹配设备刷新率,解决了定时器做动画时间间隔不稳定-不流畅的问题
事件与事件循环
JavaScript是单线程的,事件循环机制通过任务队列(Task Queue)和微任务队列(Microtask Queue)来处理异步任务 主线程执行完同步任务后,会从任务队列中取出任务执行,微任务优先于宏任务
类型
通用事件循环
事件循环是一种并发模型,本质是一个死循环 事件可以有很多种,比如文件读写事件、信号事件、超时事件、网络 I/O 事件等 以libuv IO事件件库为例,通过fileno文件描述符来指定资源,对其注册可读和可写事件,通过run执行事件队列
浏览器事件循环
浏览器->环境->子环境 在浏览器中每打开一个页面,就会分配一个它自己的环境。这样,每个页面都有自己的内存、事件循环、DOM等
Promise
同步与异步?阻塞与非阻塞?
同步与异步,针对的是任务 / 事件,是一种结果;可以类比为子循环,Node中的Tick
阻塞与非阻塞,针对的是事件循环调用栈,是一种过程;可以类比为父循环,Node中的主循环
同步非阻塞 等待结果 占用资源 同步阻塞 等待结果 施放资源 异步阻塞: 等待处理 不施放资源 异步非阻塞 不等待处理 施放资源
Promise为什么可以在状态变化之后再获取结果?
Promise本身是基于状态的订阅通知的模式,本身代表状态,当状态改变,然后通知相应的注册函数执行,本身就是一种串行结构
Promise.all和Promise.race说一下,应用场景有哪些?
Promise.all方法有用过吗?实现一下?参数里有不是promise情况怎么做?那想要返回的结果和入参一一对应该怎么做?
Promise.race
- 资源竞争 && 超时处理。类似于Array.prototype.some的机制,只要有一个执行成功,状态就改变
Promise.all
- 处理并行请求。等待所有请求完毕(成功)后执行回调,有一个没成功就会进入catch
js
必要的概念:
1、同步
2、异步
不同维度排队顺序: 主线程=>微任务队列=>宏任务队列
相同维度下:按执行速度排列
微任务队列优先级最高,大于宏任务
所以线程执行的时候是先去微任务再去宏任务
new构造函数是同步执行,所以new Promise(resolve=>{})类似的执行,属于主线程同步代码
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('1')
})
console.log("0")
js
js单线程特点
某一时间只做一个任务
主线程:同一时间只能做一件事,除此之外不能做其它的事情
主线程中的任务做完再去其它队列里抓任务来做
干活=>看任务=>
基础
定时器
js
定时器作为宏任务,在执行时,会先将定时器任务进行计时,待主线程运行完毕后,立即执行。
setTimeout(() => {
console.log("主任务队列,排列1,运行2");
}, 2000)
setTimeout(() => {
console.log("主任务队列,排列2,运行1");
}, 1000)
Promise.resolve().then(() => {
console.log("微任务队列")
})
console.log("主线程任务");
Promise微任务执行逻辑
js
Promise主体是主线程任务,Promise.then是微任务,
setTimeout(() => {
console.log("宏任务");
}, 1000);
new Promise(resolve => {
console.log("立即执行,属于主线程任务");
resolve();
}).then(resolve => {
console.log("then属于微任务");
})
DOM渲染逻辑
js
进行DOM渲染时,默认从上到下执行js代码,
如果将script标签放在头部,且js代码执行时间较长,会给用户造成一种页面有问题的感觉。
实际上执行主线程的时候是这样子的。
所以这就是为什么要将script标签放在body之后的原因。
同步
同步指的是队列顺序下的执行。
同步不等于同时。只是间隔时间比较小,最重要的是同步是有顺序的
let i = 0; //主线程任务
setTimeout(() => {
console.log(++i);
}, 1000)//宏任务队列1 使用主线程i=0,进行操作后,i=1
setTimeout(() => {
console.log(++i);
}, 1000)//宏任务队列2 使用宏任务队列1 i=1,进行操作后i=2
任务轮询
js
<div class="demo">1%</div>
<style>
.demo {
display: flex;
align-items: center;
justify-content: center;
border: solid 2px #aaa;
height: 50px;
width: 1px;
background: #a44f;
}
</style>
function handle() {
let i = 0;
let div = document.querySelector(".demo");
(function run() {
if (i < 100) {
setTimeout(() => {
++i; //关键在这,即便是执行console.log(++i),i也会在任务队列里进行++i操作
div.innerHTML = i + "%";
div.style.width = i + "px";
run();
}, 10)
}
})()
}
handle()
任务拆分
js
机器的任务拆分和人相似。
当面对大量计算时,使用拆分效果会更好。
function question_exposed() {
let num = 987654321;
let res = 0;
for (let i = 0; i <= num; i++) {
res += num--
}
console.log(res)
}
question_exposed();
console.log("this is main")
任务拆分并没有本质上减少任务量,只是对于人来说,我们能够知道进度。不过这一点十分重要
let num = 98765;
let res = 0;
function question_exposed() {
for (let i = 0; i <= num; i++) {
if (num <= 0) break;
res += num--
}
if (num > 0) {
console.log(num)
setTimeout(question_exposed)
} else {
console.log(res)
}
}
question_exposed();
console.log("this is main")
Promise微任务处理
js
有些数据需要在后台进行计算,而前台专注于任务显示。
在后台计算的数据,我们往往需要拿到数据的结果,进行后续的计算。
async function get_res() {
let res = await Promise.resolve().then(() => {
let count = 0;
for (let i = 0; i <= num; i++) {
count += num--;
}
return count;
})
console.log(res);
}
let num = 9890000;
get_res(num);
console.log("main")