事件任务类型
- 知识点
-
宏任务有:
<script>标签中的运行代码- setTimeout、setInterval的回调函数
- 事件触发的回调函数,例如
DOM Events、I/O、requestAnimationFrame、Ajax、UI交互等
注:详细来说requestAnimationFrame不属于宏任务或者微任务,每次 loop 结束发现需要渲染,在渲染之前执行的一个回调函数
-
微任务有:
- 题目
async function async1() {
console.log('1');
await async2();
console.log('2');
}
async function async2() {
console.log('3');
}
requestAnimationFrame(()=>console.log(98))
console.log('4');
async1();
setTimeout(() => {
console.log('5');
}, 0)
new Promise((resolve, reject) => {
console.log('6');
resolve();
}).then(() => {
console.log('7');
})
console.log('8');
- 题目
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// 执行顺序问题,考察频率挺高的,先自己想答案**
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
console.log(4);
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p)
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve('resolve3');
console.log('timer1')
}, 0)
resolve('resovle1');
resolve('resolve2');
}).then(res => {
console.log(res)
setTimeout(() => {
console.log(p1)
}, 1000)
}).finally(res => {
console.log('finally', res)
})
console.log('script start');
setTimeout(() => {
console.log('北歌');
}, 1 * 2000);
Promise.resolve()
.then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
async function foo() {
await bar()
console.log('async1 end')
}
foo()
async function errorFunc () {
try {
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res))
function bar() {
console.log('async2 end')
}
console.log('script end');
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
})
Promise.reject().then(() => {
console.log('13');
}, () => {
console.log('12');
})
new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
})
setTimeout(() => {
console.log('9');
Promise.resolve().then(() => {
console.log('10');
})
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12')
})
})
new Promise((resolve, reject) => {
console.log(1)
resolve()
})
.then(() => { // 微1-1
console.log(2)
new Promise((resolve, reject) => {
console.log(3)
setTimeout(() => { // 宏2
reject();
}, 3 * 1000);
resolve() // TODO 注1
})
.then(() => { // 微1-2 TODO 注2
console.log(4)
new Promise((resolve, reject) => {
console.log(5)
resolve();
})
.then(() => { // 微1-4
console.log(7)
})
.then(() => { // 微1-6
console.log(9)
})
})
.then(() => { // 微1-5 TODO 注3
console.log(8)
})
})
.then(() => { // 微1-3
console.log(6)
})
Promise.resolve()
.then(() => {
console.log('promise1');
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('timer2')
resolve()
}, 0)
})
.then(async () => {
await foo();
return new Error('error1')
})
.then((ret) => {
setTimeout(() => {
console.log(ret);
Promise.resolve()
.then(() => {
return new Error('error!!!')
})
.then(res => {
console.log("then: ", res)
})
.catch(err => {
console.log("catch: ", err)
})
}, 1 * 3000)
}, err => {
console.log(err);
})
.finally((res) => {
console.log(res);
throw new Error('error2')
})
.then((res) => {
console.log(res);
}, err => {
console.log(err);
})
})
.then(() => {
console.log('promise2');
})
function foo() {
setTimeout(() => {
console.log('async1');
}, 2 * 1000);
}
setTimeout(() => {
console.log('timer1')
Promise.resolve()
.then(() => {
console.log('promise3')
})
}, 0)
console.log('start');
async function async1() {
console.log('async1 start');
new Promise(resolve => {
try {
throw new Error('error1');
} catch (e) {
console.log(e);
}
setTimeout(() => {
// 宏3
resolve('promise4');
}, 3 * 1000);
})
.then(
res => {
// 微3-1
console.log(res);
},
err => {
console.log(err);
}
)
.finally(res => {
// 微3-2 // TODO注3
console.log(res);
});
console.log(await async2()); // 微4-1 TODO-注1
console.log('async1 end'); // 微4-2 // TODO-注2
}
function async2() {
console.log('async2');
return new Promise(resolve => {
setTimeout(() => {
// 宏4
resolve(2);
}, 1 * 3000);
});
}
console.log('script start');
setTimeout(() => {
// 宏2
console.log('setTimeout');
}, 0);
async1();
new Promise(resolve => {
console.log('promise1');
resolve();
})
.then(() => {
// 微1-2
console.log('promise2');
return new Promise(resolve => {
resolve();
}).then(() => {
// 微1-3
console.log('then 1-1');
});
})
.then(() => {
// 微1-4
console.log('promise3');
});
console.log('script end');
- 题目
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
注:### 关于73以下(金丝雀)版本和73版本的区别
- 在老版本版本以下,先执行
promise1和promise2,再执行async1。每个await后都会跟一个promise进行包裹 - 在73版本,先执行
async1再执行promise1和promise2。
为什么settimeout有时候不准时?
而setTimeout() 的第二个参数(延时时间)只是告诉 JavaScript 再过多长时间把当前任务添加到任务队列中。如果任务队列是空的,并且主线程任务执行完毕了。那么添加的代码会立即执行;如果任务队列不是空的,那么它就要等前面的代码执行完了以后再执行。
HTML5标准规定了setTimeout()的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒
浏览器的 event loop
check
浏览器里面执行一个 JS 任务就是一个 event loop,每个 loop 结束会检查下是否需要渲染,是否需要处理 worker 的消息,通过这种每次 loop 结束都 check 的方式来综合渲染、JS 执行、worker 等,让它们都能在一个线程内得到执行(渲染其实是在别的线程,但是会和 JS 线程相互阻塞)。
这样就解决了渲染、JS 执行、worker 这三者的调度问题。
但是这样有没有问题?
我们会在任务队列中不断的放新的任务,这样如果有更高优的任务是不是要等所有任务都执行完才能被执行。如果是“急事”呢?
所以这样还不行,要给 event loop 加上“急事”处理的快速通道,这就是微任务 micro tasks。
micro tasks
任务还是每次取一个执行,执行完检查下要不要渲染,处理下 worker 消息,但是也给高优先级的“急事”加入了插队机制,会在执行完任务之后,把所有的急事(micro task)全部处理完。
这样,event loop 貌似就挺完美的了,每次都会检查是否要渲染,也能更快的处理 JS 的“急事”。
requestAnimationFrame
JS 执行完,开始渲染之前会有一个生命周期,就是 requestAnimationFrame,在这里面做一些计算最合适了,能保证一定是在渲染之前做的计算。
如果有人问 requestAnimationFrame 是宏任务还是微任务,就可以告诉他:requestAnimationFrame 是每次 loop 结束发现需要渲染,在渲染之前执行的一个回调函数,不是宏微任务。
event loop 的问题
上文聊过,虽然后面加入了 worker,但是主流的方式还是 JS 计算和渲染相互阻塞,这样就导致了一个问题:
每一帧的计算和渲染是有固定频率的,如果 JS 执行时间过长,超过了一帧的刷新时间,那么就会导致渲染延迟,甚至掉帧(因为上一帧的数据还没渲染到界面就被覆盖成新的数据了),给用户的感受就是“界面卡了”。
什么情况会导致帧刷新拖延甚至帧数据被覆盖(丢帧)呢?每个 loop 在 check 渲染之前的每一个阶段都有可能,也就是 task、microtask、requestAnimationFrame、requestIdleCallback 都有可能导致阻塞了 check,这样等到了 check 的时候发现要渲染了,再去渲染的时候就晚了。
所以主线程 JS 代码不要做太多的计算(不像安卓会很自然的起一个线程来做),要做拆分,这也是为啥 ui 框架要做计算的 fiber 化,就是因为处理交互的时候,不能让计算阻塞了渲染,要递归改循环,通过链表来做计算的暂停恢复。
除了 JS 代码本身要注意之外,如果浏览器能够提供 API 就是在每帧间隔来执行,那样岂不是就不会阻塞了,所以后来有了 requestIdeCallback。
requestIdleCallback
requestIdleCallback 会在每次 check 结束发现距离下一帧的刷新还有时间,就执行一下这个。如果时间不够,就下一帧再说。
如果每一帧都没时间呢,那也不行,所以提供了 timeout 的参数可以指定最长的等待时间,如果一直没时间执行这个逻辑,那么就算拖延了帧渲染也要执行。
这个 api 对于前端框架来说太需要了,框架就是希望计算不阻塞渲染,也就是在每一帧的间隔时间(idle时间)做计算,但是这个 api 毕竟是最近加的,有兼容问题,所以 react 自己实现了类似 idle callback 的fiber 机制,在执行逻辑之前判断一下离下一帧刷新还有多久,来判断是否执行逻辑。
总结
总之,浏览器里有 JS 引擎做 JS 代码的执行,利用注入的浏览器 API 完成功能,有渲染引擎做页面渲染,两者都比较纯粹,需要一个调度的方式,就是 event loop。
event loop 实现了 task 和 急事处理机制 microtask,而且每次 loop 结束会 check 是否要渲染,渲染之前会有 requestAnimationFrames 生命周期。
帧刷新不能被拖延否则会卡顿甚至掉帧,所以就需要 JS 代码里面不要做过多计算,于是有了 requestIdleCallback 的 api,希望在每次 check 完发现还有时间就执行,没时间就不执行(这个deadline的时间也作为参数让 js 代码自己判断),为了避免一直没时间,还提供了 timeout 参数强制执行。
防止计算时间过长导致渲染掉帧是 ui 框架一直关注的问题,就是怎么不阻塞渲染,让逻辑能够拆成帧间隔时间内能够执行完的小块。浏览器提供了 idelcallback 的 api,很多 ui 框架也通过递归改循环然后记录状态等方式实现了计算量的拆分,目的只有一个:loop 内的逻辑执行不能阻塞 check,也就是不能阻塞渲染引擎做帧刷新。所以不管是 JS 代码宏微任务、 requestAnimationCallback、requestIdleCallback 都不能计算时间太长。这个问题是前端开发的持续性阵痛。
nodejs应用
nodejs线程
- node是单线程也指的是主线程是单线程的
- node进程创建了哪些线程
-
Javascript 执行主线程
-
watchdog 监控线程用于处理调试信息
-
v8 task scheduler 线程用于调度任务优先级,加速延迟敏感任务执行
-
4 个 v8 线程 主要用来执行代码调优与 GC 等后台任务;以及用于异步 I / O 的 libuv 线程池。
-
nodejs事件循环
浏览器的 Event Loop 只分了两层优先级,一层是宏任务,一层是微任务。但是宏任务之间没有再划分优先级,微任务之间也没有再划分优先级。
而 Node.js 宏任务之间也是有优先级的,比如定时器 Timer 的逻辑就比 IO 的逻辑优先级高,因为涉及到时间,越早越准确;而 close 资源的处理逻辑优先级就很低,因为不 close 最多多占点内存等资源,影响不大。
于是就把宏任务队列拆成了五个优先级:Timers、Pending、Poll、Check、Close。
解释一下这五种宏任务:
Timers Callback: 涉及到时间,肯定越早执行越准确,所以这个优先级最高很容易理解。
Pending Callback:处理网络、IO 等异常时的回调,有的系统会等待发生错误的上报,所以得处理下。
Poll Callback:处理 IO 的 data,网络的 connection,服务器主要处理的就是这个。
Check Callback:执行 setImmediate 的回调,特点是刚执行完 IO 之后就能回调这个。
Close Callback:关闭资源的回调,晚点执行影响也不到,优先级最低。
还有一点不同要特别注意:
Node.js 的 Event Loop 并不是浏览器那种一次执行一个宏任务,然后执行所有的微任务,而是执行完一定数量的 Timers 宏任务,再去执行所有微任务,然后再执行一定数量的 Pending 的宏任务,然后再去执行所有微任务,剩余的 Poll、Check、Close 的宏任务也是这样。
其实按照优先级来看很容易理解:
假设浏览器里面的宏任务优先级是 1,所以是按照先后顺序依次执行,也就是一个宏任务,所有的微任务,再一个宏任务,再所有的微任务。
而 Node.js 的 宏任务之间也是有优先级的,所以 Node.js 的 Event Loop 每次都是把当前优先级的所有宏任务跑完再去跑微任务,然后再跑下一个优先级的宏任务。
也就是是一定数量的 Timers 宏任务,再所有微任务,再一定数量的 Pending Callback 宏任务,再所有微任务这样。
为什么说是一定数量呢?
因为如果某个阶段宏任务太多,下个阶段就一直执行不到了,所以有个上限的限制,剩余的下个 Event Loop 再继续执行。
除了宏任务有优先级,微任务也划分了优先级,多了一个 process.nextTick 的高优先级微任务,在所有的普通微任务之前来跑。 所以,Node.js 的 Event Loop 的完整流程就是这样的:
- Timers 阶段:执行一定数量的定时器,也就是 setTimeout、setInterval 的 callback,太多的话留到下次执行
- 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
- Pending 阶段:执行一定数量的 IO 和网络的异常回调,太多的话留到下次执行
- 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
- Idle/Prepare 阶段:内部用的一个阶段
- 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
- Poll 阶段:执行一定数量的文件的 data 回调、网络的 connection 回调,太多的话留到下次执行。如果没有 IO 回调并且也没有 timers、check 阶段的回调要处理,就阻塞在这里等待 IO 事件
- 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
- Check 阶段:执行一定数量的 setImmediate 的 callback,太多的话留到下次执行。
- 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
- Close 阶段:执行一定数量的 close 事件的 callback,太多的话留到下次执行。
- 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
还有一个特别要注意的点,就是 poll 阶段:如果执行到 poll 阶段,发现 poll 队列为空并且 timers 队列、check 队列都没有任务要执行,那么就阻塞的等在这里等 IO 事件,而不是空转。 这点设计也是因为服务器主要是处理 IO 的,阻塞在这里可以更早的响应 IO。
完整的event loop
process.nextTick、setImmediate、setTimeout对比
process.nextTick
setTimeout(()=>{
console.log('TIMEOUT FIRED');
}, 0)
new Promise((resolve=>{
resolve(2)
})).then((res)=>{
console.log('promise',res);
})
process.nextTick(()=>{
console.log(1);
process.nextTick(()=>{console.log(3);});
});
/*
1
3
promise 2
TIMEOUT FIRED
*/
复制代码
setImmediate
setImmediate(()=> {
console.log(1);
setImmediate(()=>{console.log(2);});
});
setTimeout(()=> {
console.log('TIMEOUT FIRED');
}, 0);
复制代码
这个结果不固定,同一台机器测试结果也有两种:
// TIMEOUT FIRED =>1 =>2
或者
// 1=>TIMEOUT FIRED=>2
复制代码
复制代码
- 事件队列进入timer,性能好的 小于1ms,则不执行回调继续往下。若此时大于1ms, 则输出 TIMEOUT FIRED 就不输出步骤3了。
- poll阶段任务为空,存在setImmediate 直接进入setImmediate 输出1
- 然后再次到达timer 输出 TIMEOUT FIRED
- 再次进入check 阶段 输出 2
原因在于setTimeout 0 node 中至少为1ms,也就是取决于机器执行至timer时是否到了可执行的时机。
做个对比就比较清楚了:
setImmediate(()=> {
console.log(1);
setImmediate(()=>{console.log(2);});
});
setImmediate(()=>{console.log(4);});
setTimeout(()=> {
console.log('TIMEOUT FIRED');
}, 20);
// 1=>2=>TIMEOUT FIRED
复制代码
此时间隔时间较长,timer阶段最后才会执行,所以会先执行两次check,出处1,2 下面再看个例子 poll阶段任务队列
var fs = require('fs')
fs.readFile('./yarn.lock', () => {
setImmediate(() => {
console.log('1')
setImmediate(() => {
console.log('2')
})
})
setTimeout(() => {
console.log('TIMEOUT FIRED')
}, 0)
})
// 结果确定:
// 输出始终为1=>TIMEOUT FIRED=>2
复制代码
复制代码
- 读取文件,回调进入poll阶段
- 当前无任务队列,直接check 输出1 将setImmediate2加入事件队列
- 接着timer阶段,输出TIMEOUT FIRED
- 再次check阶段,输出2 结论:
process.nextTick(),效率最高,消费资源小,但会阻塞CPU的后续调用;
setTimeout(),精确度不高,可能有延迟执行的情况发生,且因为动用了红黑树,所以消耗资源大;
setImmediate(),消耗的资源小,也不会造成阻塞,但效率也是最低的。
- 题目:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
如果node版本为v11.x, 其结果与浏览器一致。
start
end
promise3
timer1
promise1
timer2
promise2
复制代码
具体详情可以查看《又被node的eventloop坑了,这次是node的锅》。
如果v10版本上述结果存在两种情况:
- 如果time2定时器已经在执行队列中了
start
end
promise3
timer1
timer2
promise1
promise2复制代码
- 如果time2定时器没有在执行对列中,执行结果为
start
end
promise3
timer1
promise1
timer2
promise2复制代码
-
Process.nextTick()
process.nextTick()虽然它是异步API的一部分,但未在图中显示。这是因为process.nextTick()从技术上讲,它不是事件循环的一部分。process.nextTick()方法将callback添加到next tick队列。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。
换种理解方式:
- 当每个阶段完成后,如果存在
nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。
例子
let bar;
setTimeout(() => {
console.log('setTimeout');
}, 0)
setImmediate(() => {
console.log('setImmediate');
})
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;复制代码
在NodeV10中上述代码执行可能有两种答案,一种为:
bar 1
setTimeout
setImmediate复制代码
另一种为:
bar 1
setImmediate
setTimeout复制代码
无论哪种,始终都是先执行process.nextTick(callback),打印bar 1。
事件流和事件循环
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log(1))
console.log(2);
}, false);
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log(3))
console.log(4);
}, false);
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log(1))
console.log(2);
}, false);
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log(3))
console.log(4);
}, true);
document.body.addEventListener('click', () => {
setTimeout(() => {
console.log(1);
})
console.log(2);
}, false);
document.body.addEventListener('click', () => {
setTimeout(() => {
console.log(3);
})
console.log(4);
}, true);