目录
3.2 Vue 为什么要优先使用微任务实现 nextTick
4. Node.js 中的 process.nextTick
5.1 setTimeout / setInterval 的执行时间为啥无法确定呢?
5.2 setTimeout / setInterval 概念容易踩得坑
5.2.3 setTimeout 模拟实现 setInterval
5.2.4 setInterval 模拟实现 setTimeout
5.3 requestAnimationFrame 浏览器提供的动画 API
5.3.1 setTimeout / setInterval 动画卡顿
5.3.2 什么是 requestAnimationFrame
5.3.3 setTimeout VS setInterval VS requestAnimationFrame
HTML5 中的 WebWoker,号称 JavaScript 多线程版
其实本质上还是用 JavaScript 单线程模拟出来的,所以搞明白 Event Loop 很重要
1. 事件轮询机制(Event Loop)是什么
1.1 宏任务、微任务
任务可以分成两种:
- 宏任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务
- 微任务:不进入主线程、而进入"微任务列表" 的任务
宏任务(Macrotasks)包括:
- script 全部代码(注意:同步代码也属于宏任务)
- setTimeout
- setInterval
- setImmediate
微任务(Microtasks)包括:
- Promise
- MutationObserver
不同类型的任务,会进入对应的 Event Queue,比如 setTimeout 和 setInterval 会进入相同的 Event Queue
1.2 Event Loop 循环过程
JavaScript 是单线程的,同一时间只能做一件事。所有任务都需要排队,前一个任务结束,才会执行后一个任务(JavaScript 是一门单线程语言,Event Loop 是 JavaScript 的执行机制)
- 宏任务是一个个执行;
- 如果执行过程中,遇到微任务,就把他加入微任务队列;
- 当前宏任务执行完后,会判断 微任务列表 中是否有任务;
- 如果有,就把该微任务放到主线程中执行;如果没有,就继续执行下一个宏任务,不断循环;
用 Ajax 举个栗子:
- Ajax 作为微任务,进入微任务列表,注册回调函数 success
- 执行同步代码,console.log('代码执行结束')
- 等待 Ajax 请求完成,主线程从 微任务列表 读取回调函数 success 并执行
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
1.3 经典题目分析
Promise.resolve()
.then(function() {
console.log("promise0"); // 2 执行微任务1 Promise1
})
.then(function() {
console.log("promise5"); // 4 执行微任务3 Promise3
});
setTimeout(() => {
console.log("timer1"); // 5 执行宏任务 setTimeout1
Promise.resolve().then(function() {
console.log("promise2"); // 6 执行宏任务中的微任务
});
Promise.resolve().then(function() {
console.log("promise4"); // 7 执行宏任务中的微任务
});
}, 0);
setTimeout(() => {
console.log("timer2"); // 8 执行宏任务 setTimeout2
Promise.resolve().then(function() {
console.log("promise3"); // 9 执行宏任务中的微任务
});
}, 0);
Promise.resolve().then(function() {
console.log("promise1"); // 3 执行微任务2 Promise2
});
console.log("start"); // 1 执行同步代码
打印结果: start ------ promise0 ------ promise1 ------ promise5 ------ timer1 ------ promise2 ------ promise4 ------ timer2 ------ promise3
1.3.1 第一轮事件循环
- 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 start
- 遇到 Promise,其回调函数 then 被分发到 微任务队列 中。记为 Promise1
- 遇到 Promise,其回调函数 then 被分发到 微任务队列 中。记为 Promise2
- 遇到 setTimeout,分发到 宏任务队列。记为 setTimeout1
- 遇到 setTimeout,分发到 宏任务队列。记为 setTimeout2
- 遇到 Promise,其回调函数 then 被分发到 微任务队列 中。记为 Promise3
宏任务队列:
- setTimeout1
- setTimeout2
微任务队列:
- Promise1
- Promise2
- Promise3
执行同步代码,输出 start
执行微任务队列,输出 promise0 ------ promise1 ------ promise5
第 2、3、4 步的输出可能会有疑问
这里举个栗子~
new Promise(function(resolve) {
// 这里作为同步代码,立即执行
console.log('promise');
}).then(function() {
// then 中的代码,会被放入微任务队列
console.log('then');
})
可以看出:
- new Promise 中的代码,是立即执行的
- 而回调函数 then,会被放到微任务队列中
1.3.2 第二、三次事件循环
第二次事件循环:
- 执行第二个宏任务 setTimeout1
- 先执行同步代码,输出 timer1
- 再将微任务放到微任务列表中,由于主线程没有代码可执行,所以将微任务放到主任务线程中,输出 promise2 ------ promise4
第三次事件循环:
- 执行第三个宏任务 setTimeout2
- 先执行同步代码,输出 timer2
- 再将微任务放到微任务列表中,由于主线程没有代码可执行,所以将微任务放到主任务线程中,输出 promise3
1.3.3 参考文章
这篇文章的示例讲解比较清楚,值得学习
2. async、await 在事件轮询中的执行时机
async 隐式返回 Promise,会产生一个微任务
await 后面的代码,在执行微任务时执行
举个栗子 ~~ 看下面的代码
console.log("script start"); // 1 同步代码
async function async1() {
await async2(); // 2 同步代码
console.log("async1 end"); // 5 这里的执行时机:在执行微任务时执行
}
async function async2() {
console.log("async2 end");
}
async1();
setTimeout(function() {
console.log("setTimeout"); // 8 第二个宏任务
}, 0);
new Promise(resolve => {
console.log("Promise"); // 3 同步代码
resolve();
})
.then(function() {
console.log("promise1"); // 6 微任务
})
.then(function() {
console.log("promise2"); // 7 微任务
});
console.log("script end"); // 4 同步代码
打印结果: script start ------ async2 end ------ Promise ------ script end ------ async1 end ------ promise1 ------ promise2 ------ setTimeout
第一次事件循环:
- 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
- 执行 async1,里面的 await async2 是个同步代码,所以打印了 async2 end;同时,async1 还产生一个微任务(就是 await 后面的代码),放进微任务列表,暂时不执行
- 继续执行主任务,走到 promise,走里面的同步代码,打印了 Promise,里面的 then 放进微任务列表,暂时不执行
- 继续执行主任务,执行同步代码 console.log...,打印了 script end
- 此时微任务列表里,首个微任务是 await 后面的代码,打印了 async1 end;剩下两个微任务是 promise 里的,打印了 promise1、promise2
- 第一个宏任务执行完毕
第二次事件循环:
- 执行第二个宏任务 setTimeout,打印了 setTimeout
3. 浏览器更新渲染时机、Vue nextTick
3.1 Event Loop 与 浏览器更新渲染时机
浏览器更新渲染,会在 Event Loop 中的 宏任务、微任务 完成后进行
先宏任务,再微任务,然后再渲染更新
宏任务队列中,如果有大量任务等待执行时,应该将 修改 DOM 作为微任务,这样就能在 本次事件轮询中 更新 DOM(Vue nextTick 就是这么实现的),更快的将 页面变化 呈现给用户
3.2 Vue 为什么要优先使用微任务实现 nextTick
Vue3 nextTick 源码地址:core\packages\runtime-core\src\scheduler.ts
Vue nextTick 的码的优先级判断:Promise > MutationObserver > setImmediate > setTimeout
答:根据 Event Loop 与浏览器更新渲染时机可得:
- 优先使用 Promise 等微任务,本次事件轮询就能获得更新的 DOM
- 如果使用宏任务,要到下一次事件轮询中,才能获得更新的 DOM
学习Vue3 第三十五章(Evnet Loop 和 nextTick)_小满zs的博客-CSDN博客在我们学习nextTick 之前需要先了解Event Loop 时间循环机制在我们学js 的时候都知道js 是单线程的如果是多线程的话会引发一个问题在同一时间同时操作DOM 一个增加一个删除JS就不知道到底要干嘛了,所以这个语言是单线程的但是随着HTML5到来js也支持了多线程webWorker 但是也是不允许操作DOM单线程就意味着所有的任务都需要排队,后面的任务需要等前面的任务执行完才能执行,如果前面的任务耗时过长,后面的任务就需要一直等,一些从用户角度上不需要等待的任务就会一直等待,这个从体验角度上来https://xiaoman.blog.csdn.net/article/details/125237755https://github.com/qingzhou729/study/issues/15https://github.com/qingzhou729/study/issues/15
4. Node.js 中的 process.nextTick
4.1 什么是 process.nextTick
process.nextTick 相当于 Node.js 中的 setTimeout,是 Node.js 定义的一种机制,它有自己的 nextTickQueue
process.nextTick 不能完全当作 JavaScript 中的微任务,process.nextTick 执行顺序早于微任务
举个栗子 ~~ 看下面的代码:
console.log("start"); // 1 同步代码
setTimeout(() => {
console.log("setTimeout"); // 6 第二个宏任务
}, 0);
Promise.resolve().then(() => {
// 4 第一个微任务,早于 process.nextTick 内部的 promise 放入微任务列表
console.log("promise");
});
process.nextTick(() => {
console.log("nextTick"); // 3 Node.js process.nextTick 早于微任务执行
Promise.resolve().then(() => {
console.log("promise1"); // 5 第二个微任务
});
});
console.log("end"); // 2 同步代码
执行结果:start ------ end ------ nextTick ------ promise ------ promise1 ------ setTimeout
4.2 浏览器事件循环 VS Node 事件循环
5. setTimeout VS setInterval
- setTimeout 固定时长后执行
- setInterval 间隔固定时间后重复执行
5.1 setTimeout / setInterval 的执行时间为啥无法确定呢?
举个栗子~
定时器内部代码,需要进行大量计算,或者 DOM 操作,代码执行时间 超过了 定时器设置的时间
再举个栗子~
setTimeout 早早的把 task() 放进等待队列了,但是由于 JavaScript 是单线程的,只能等 sleep() 执行完毕(时间远超 3s),才能执行 task()
setTimeout(() => {
// 控制台执行 task() 需要的时间,远远超过 3 秒
task()
}, 3000)
sleep(10000000)
5.2 setTimeout / setInterval 概念容易踩得坑
5.2.1 setTimeout 举个栗子~
setTimeout(fn, 0),可以立即执行吗?答案是不行。
- setTimeout 指的是:主线程的任务都执行完了,再等 x 秒,才能执行内部代码;
- 即使主线程为空,也不能立即执行,因为 JavaScript 规定 setTimeout、setInterval 最短时长为 4ms;
5.2.2 setInterval 举个栗子~
对于 setInterval(fn, ms) 来说,他是每过 ms 秒就一定会执行一次 fn 吗?答案是不是。
- 是每过 ms 秒,会创建一个 fn 进入任务队列;
- 一旦 setInterval 的回调函数 fn 实际执行时间,超过了延迟时间 ms,那么就完全看不出来有时间间隔了;
5.2.3 setTimeout 模拟实现 setInterval
// 使用闭包实现
function mySetInterval(fn, t) {
let timer = null;
function interval() {
fn();
timer = setTimeout(interval, t);
}
interval();
return {
// cancel用来清除定时器
cancel() {
clearTimeout(timer);
}
};
}
5.2.4 setInterval 模拟实现 setTimeout
function mySetTimeout(fn, time) {
let timer = setInterval(() => {
clearInterval(timer);
fn();
}, time);
}
// 使用
mySetTimeout(() => {
console.log(1);
}, 2000);
5.3 requestAnimationFrame 浏览器提供的动画 API
5.3.1 setTimeout / setInterval 动画卡顿
setTimeout / setInterval 通过设置一个间隔时间,不断改变 css 实现动画效果,在不同设备上可能会出现卡顿、抖动等现象,这是因为:
- 不同设备的屏幕,刷新频率可能不同
- setTimeout / setInterval 只能设置固定的时间间隔,这个时间 与 屏幕刷新间隔可能不同
5.3.2 什么是 requestAnimationFrame
HTML5 新增 API,类似于 setTimeout,window.requestAnimationFrame
requestAnimationFrame 告诉浏览器,在下次重绘之前,执行传入的回调函数(通常是操作 DOM,更新动画的函数),也就是说:刷新频率与显示器的刷新频率保持一致
requestAnimationFrame 是浏览器专门为 动画(DOM 动画、Canvas 动画、 SVG 动画、WebGL 动画) 提供的API,使用该 API 可避免使用setTimeout / setInterval 造成的动画卡顿
5.3.3 setTimeout VS setInterval VS requestAnimationFrame
引擎层面的区别:
- setTimeout / setInterval 属于 JavaScript 引擎,存在事件轮询
- requestAnimationFrame 属于 GUI 引擎
JavaScript 引擎与 GUI 引擎是互斥的,也就是说:
- GUI 引擎在渲染时,会阻塞 JavaScript 引擎的计算;
- 如果在 GUI 渲染的时候,JavaScript 又同时改变了DOM,就会造成页面渲染不同步;
性能层面的区别:
- 当页面被隐藏或最小化时,定时器 setTimeout 仍会在后台运行,执行动画任务
- 当页面被隐藏或最小化时,屏幕刷新任务会被系统暂停,requestAnimationFrame 也会停止