一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
浏览器
浏览器是提供了多个线程 浏览器同时也是多进程的,比如浏览器的每个tab标签页都是一个独立的渲染进程 在浏览的线程下又有: js引擎线程, HTTP请求线程, 定时触发线程, 时间触发线程, GUI线程,这些线程为js在浏览器中完成异步任务提供了技术基础
事件驱动
浏览器异步触发的原理实际上背后是一套时间驱动的机制 事件触发,任务选择,任务执行都是事件驱动来完成的,nodejs 和浏览器的设计都是基于事件驱动的 事件循环就是在事件驱动模式中来管理和执行事件的一套流程
Even Loop即事件循环
首先js的是单线程的,浏览器是多线程,执行js代码的线程只有一个是浏览器提供的js引擎线程 如何在执行的过程中不造阻塞,浏览器的node提供了事件循环机制来防止js单线程运行时 ,浏览器和node在执行js单线程时不会阻塞的一种机制,同时事件事件循环机制也就是我们经常使用异步的原理
浏览器中的事件循环
在js中,任务被分为两种,一种宏任务,一种微任务
宏任务和微任务
1. 宏任务:
- setTimeout
- setInterval
- setImmediate (浏览器暂时不支持,只有IE10支持,具体可见
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
2. 微任务:
- Promise async awit
- Object.observe
- MutationObserver
对比: 宏任务特征: 有明确的异步任务需要执行和回调,需要其他异步线程支持 微任务特征: 没有明确的异步任务需要执行,只有回调,不需要其他异步线程支持
3. 为什么区分宏任务和微任务
- 调用栈
调用栈是一个后进先出
的数据结构,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移除,直到栈内被清空
- 任务队列
即队列,是一种先进先出的一种数据结构。
同步任务和异步任务
js单线程任务被分为同步任务和异步任务
同步任务会在调用栈中按照顺序等待主线程依次执行 异步任务会在异步任务有了结果之后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行
为什么区分?
异步 调用栈 消息队列 setTimeOut 微任务队列 执行完了之后就会evenloop循环
微任务比宏任务先执行 宏任务比微任务之间隔了一个DOM渲染
为什么要区分红任务和微任务 任务队列 先进先出 如果有优先级的任务引入微任务
node中的事件循环
将回调添加到轮询队列中以最终的执行
- 循环 + 任务队列的流程
- 微任务优先于宏任务
nodejs中其他常见的异步形式 :
- 文件I/O读取-异步加载本地文件
- setimmediate() -与settimeout设置0ms类似,在某些同步代码完成后立马执行
- process.nextTick()- 在某些同步任务完成后立即执行
- server close 等关闭回调
nodejs中的事件循环主要是在libuv库中执行的
nodejs的跨平台和事件循环机制都是预约libuv库的
libuv库怎么循环 :
- timers阶段,执行所有settimeout() setinterval()的回调
- pending callback 某些系统操作额的回调(比如tcp连接错误)
- idle prepare 仅是node内部使用
- poll 轮训等待新的链接和请求等事件,执行I/O回调等
- check setimmediate回调函数执行
- close callback 关闭回调执行,如socket.on('close',...)
实际上在node v10及以前的过程:
- 执行完一个阶段中的所有任务
- 执行nextTick队列里的内容
- 执行完微任务队的内容
但是在node v10以后和浏览器
的行为一致了
timers
执行setTimeout
和setInterval
中到期的callback
,执行这两者需要设置一个毫秒数,理论上应该是时间一到就立即执行callback回调,但那是由于system的调度可能会延时,达不到预期的时间
官方文档的例子:
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
复制代码
当进入时间循环时,他有一个空队列(fs.readFile()尚未完成),因此定时器将等待剩余毫秒数,当达到95ms时u,fs.readFile()完成读取文件并其完成需要10毫秒的回调被添加到轮询队列并执行
当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后会回到timers阶段以执行定时器的回调
因此在此实例中,将看到正在调度的计时器与正在执行的回调之间的总延时将为105ms
poll
该poll阶段有两个主要功能
- 执行
I/O
回调 - 处理轮询队列中的事件(回到timer阶段执行回调)
当事件循环进入poll阶段并且在timer
中没有可以执行定时器时,将发生以下两种情况之一:
- 如果poll队列不为空
- 事件循环将遍历其同步执行他们的callback队列,直到队列为空,或者达到
system-dependent
(系统相关限制)
2.poll队列为空,则会发生以下两种情况
- 如果setImmediate()回调需要执行,则会立即停止执行poll阶段并进入执行check阶段以执行回调
- 如果没有setImmediate()回调需要执行,会等待回调被加入到队列中并立即执行回调,这里也有个超时的设置防止一直等待下去
设定了timer的话且poll队列为空,则会判断是否有timer超时,如果有的话回到timers阶段执行回调
check
此阶段允许人员在poll阶段完成后立即执行回调
setImmediate()
的回调会被加入chenk队列中,从event loop 的阶段图可以知道,check阶段我的执行顺序在poll阶段后,如下例子
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')
复制代码
start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
思考
- 每一轮Eventloop都会伴随着渲染吗
requestAninmationFrame
在哪个阶段执行,在渲染之前还是渲染之后?在microTask
前还是后requestIdleCallback
在哪个阶段执行?如何去执行?在渲染前还是后?在microTask
的前还是后?resize
、scroll
这些事件都是如何去派发的
总结事件循环
定义 事件循环是为了协调事件,用户交互,脚本,渲染,网络任务
- 从宏任务队列中取出一个宏任务并执行
- 检查微任务队列,执行并且清空微任务队列,如果在微任务队列中又加入了新的微任务,也会在这一步一起执行
- 进入更新渲染阶段,判断是否需要渲染,这里有rendering opportunity 的概念,也就是说不一定每一轮eventloop都会对应一次浏览器渲染,要根据屏幕刷新率,页面性能,页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的(所以多个task很可能再一次渲染之间执行)
- 浏览器会尽可能的保持帧率稳定,例如页面性能无法维持60fps(每16.66ms渲染一次)的话,那么浏览器就会选择30fps的个更新速率,而不是偶尔丢帧
- 如果浏览器上下文不可见,那么页面会降低到4fps左右甚至更低
- 如果满足以下条件也会跳过渲染
- 浏览器判断更新渲染不会带来视觉上的改变
- 帧动画回调为空,可以通过
requestAninmationFrame
来请求帧动画
- 如果上述判断决定本来不需要渲染,那么下面几部也不继续运行
- 对于需要渲染的文档,如果窗口的大小发生了变化,执行监听的
resize
方法 - 对于需要渲染的文档,如果页面发生了滚动,执行
scroll
方法 - 对于需要渲染的文档,执行帧动画回调,也就是
requestAninmationFrame
的回调 - 对月需要渲染的文档,执行IntersectionObserver (当其监听到目标元素的可见部分穿过了一个或多个阈(thresholds)时,会执行指定的回调函数。)
- 对于需要渲染的文档,重新渲染绘制用户界面
- 判断task队列和microTask队列是否都为空,如果是的话则进行
Idle
空闲周期的算法,判断是否要执行requestIdleCallback
的回调函数
对于resize和scroll来说,并不是到了这一步采取执行滚动和缩放