简析浏览器和node的EventLoop机制
前置知识
- 进程
- cpu承担了所有计算任务
- 进程是cpu资源分配的最小单位
- 在同一个时间内,单个cpu只能执行一个任务,只能运行一个进程。
- 如果有一个进程正在执行,其他进程就要暂停
- 线程
- 线程是cpu调度的最小单位
- 一个进程可以包含多个线程,这些线程共享这个进程的资源
- 浏览器
- 浏览器是多进程
- 每个tab页就是一个进程
- 进程分类
- 主进程
- 主进程控制着子进程的创建和销毁
- 浏览器界面的显示如用户交互,前进后退
- 将要渲染的内容绘制到页面上
- 渲染进程
- 负责页面的渲染 脚本执行,事件处理
- 每个tab页都有一个渲染进程
- 网络进程
- GPU进程
- 渲染进程
- GUI渲染线程
- 渲染布局和绘制页面
- 页面重绘和回流时会触发此线程进行重排或者渲染
- 与js引擎互斥
- JS引擎线程
- 负责解析执行js脚本
- 单线程
- 与gui线程互斥
- 事件触发线程
- 用来控制事件循环,将事件触发的一些宏任务和微任务放入对应的任务队列
- 定时器触发线程
- 定时任务由定时器线程计时
- 计时完成后会通知事件触发线程
- 异步http请求线程
- 浏览器有一个单独的线程处理ajax请求
- 当请求完毕后如果有回调,会通知事件触发线程
浏览器的事件循环机制
- js分为同步任务和异步任务
- 同步任务都在js引擎线程上执行,形成一个执行栈
- 事件触发线程会管理一个任务队列,满足条件的异步任务将其回调函数放入任务队列中。
- 执行栈中所有同步任务执行完毕,js引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行。
- 举例
- setTimout/setInterval js引擎线程执行js代码,遇到setTimout/setInterval开始由定时器触发线程计时,计时完成后由事件触发线程将回调函数放入宏任务队列
- ajax js引擎线程执行js代码,遇到ajax开始由异步http请求线程处理请求,请求完成后由事件触发线程将回调函数放入宏任务队列
- 执行顺序
- 浏览器请求回js脚本后,开启js脚本的解析,首先会将整个js脚本作为一个宏任务入主执行栈
- 执行过程中遇到同步代码直接执行,遇到异步代码(等到其获取到结果或者计时完成时)将其回调函数放入对应的任务队列中
- 当前宏任务执行完后出栈,检查微任务队列,有则全部执行完成
- 渲染前执行requestAnimationFrame回调
- 执行浏览器UI线程的渲染工作
- 执行完本轮的宏任务 然后依此循环直到所有宏任务微任务都执行完成
常见的宏任务
- script
- setTimeout/setInterval
- MessageChannel
- postMessage
- setImmediate(node)
常见的微任务
- promise
- MutationObserver
- Object.observe
- process.nextTick(node)
setTimeout存在的问题
- 当前任务可能会影响定时器任务的执行
- 嵌套的定时器最小间隔为4ms
- 队列前面已经加入了其他任务,动画代码就要等前面的任务完成后再执行
requestAnimationFrame
- requestAnimationFrame使用一个回调函数作为参数会在下次页面重绘前执行
- requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中完成
- requestAnimationFrame的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
- 不可见的元素requestAnimationFrame将不会进行重绘或回流(减少CPU、GPU和内存使用量)
- 运行时浏览器会自动优化方法的调用(如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销)
- 应用场景
- 监听 scroll 函数
- 平滑滚动到页面顶部
- 大量数据渲染
- 监控卡顿方法(页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿)
- 测量 fps 值,如果连续出现几个 fps 值 ≤ 阈值,则认为是卡顿
requestIdleCallback
- requestIdleCallback 是利用帧之间空闲时间来执行JS,定位是不重要以及不紧急的任务
- 使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
- 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿
- 1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms

- 一帧内做的事情
- 响应输入事件
- 执行js代码
- 开始一帧
- 执行requestAnimationFrame回调
- 布局
- 绘制
- 空闲(requestIdleCallback回调)?
- requestIdleCallback接收一个回调callback,即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
- didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
- timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
- requestIdleCallback发生在一帧的最后,此时页面布局已经完成,所以不建议在 requestIdleCallback 里再操作 DOM,这样会导致页面再次重绘,DOM 操作建议在 requestAnimationFrame 中进行。
Node中的EventLoop
- Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv
- libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API
- V8引擎解析JavaScript脚本并调用Node API
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
- V8引擎再将结果返回给用户
Node事件循环过程
- 在node应用程序启动后,先执行同步代码,清空主执行栈后,在进入事件循环之前会先走nextTick的回调然后进入事件循环,检查timer是否有回调函数有的话请空所有定时器回调然后清空当前的微任务队列,timer阶段如果没有到时间的回调,检查 poll 的回调函数队列:不为空,则遍历执行直到队列为空(执行现阶段所有的微任务)。为空,则检查是否有 setImmediate 回调,有则进入 check 阶段执行回调(然后执行现阶段所有的微任务),check阶段没有setImmediate回调,事件循环会阻塞在pull阶段不断轮巡timer和check阶段,看有没有等待执行的回调。
- 之所以会停在pull阶段可以看出node是事件i/o优先的
node 与浏览器 eventloop 的差异
- 浏览器中微任务的队列是每个宏任务执行完之后执行。
- node 中的微任务会在事件循环的各个阶段之间执行,也就是一个阶段(如timer, pull, check)执行完毕就会去执行微任务队列里面的任务。