Skip to content

Latest commit

 

History

History
103 lines (72 loc) · 6.48 KB

事件循环.md

File metadata and controls

103 lines (72 loc) · 6.48 KB

事件循环

是什么

  • js 是设计成单线程的,为了更好的处理异步任务,所以设计了事件循环机制

js 为什么是单线程

  • 与 js 的用途有关
  • 如果 js 被设计了多线程,如果有一个线程要修改一个 dom 元素,另一个线程要删除这个 dom 元素,渲染器就不知道以哪个线程为准,DOM 渲染的结果不可预期
  • 多线程具有复杂性,编码的复杂性会增高
  • 为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质

同步任务和异步任务

  • JavaScript 单线程任务分为同步任务和异步任务
  • JavaScript 有一个主线程(js 引擎线程)和调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行,同步任务会在调用栈中按顺序依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中,等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行
  • 在遇到异步任务时(setTimeout、DOM 事件、ajax 等),会转交给浏览器的其他工作线程(事件触发线程、定时器线程)执行,执行完之后将回调函数放入到任务队列
  • 事件触发线程管理一个任务队列,调用栈中的代码调用某些异步 API 时会在任务队列中添加事件,异步任务触发条件达成,将回调事件放到任务队列中
  • 当主线程的任务执行完了(执行栈空了),js 会去询问事件队列有没有回调函数需要执行(所以 setTimeout 0 会等到最后才执行)
  • 如此循环往复,形成事件循环机制
  • 事件循环不一定每轮都伴随着重渲染,但是如果有微任务,一定会伴随着微任务执行

宏任务和微任务

  • 异步任务队列又分微任务队列和宏任务队列

  • 宏任务是由宿主发起的,而微任务由 JavaScript 自身发起

  • 宏任务(多为运行环境 api):script 标签中的代码、UI 渲染、UI 交互、setTimeout/setInterval/setImmediate、DOM 事件、postMessage、ajax 请求、requestAnimationFrame

    宏任务代表一个个离散的、独立工作单元,运行完任务后,浏览器可以继续其他调度,如重新渲染页面的 UI 或执行垃圾回收

  • 微任务(多为语法):Promise.then\catch\finally、MutationObserver、async/await、process.nextTick(NodeJS)

    微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的 UI。微任务的案例包括 promise 回调函数、DOM 发生变化等。微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染 UI 之前执行指定的行为,避免不必要的 UI 重绘,UI 重绘会使应用程序的状态不连续

  • 两者区别:

    • 宏任务:DOM 渲染后触发
    • 微任务:DOM 渲染前触发
  • 当满足执行条件时,宏任务(macroTask) 和 微任务(microTask) 会各自被放入对应的队列:宏队列(Macrotask Queue) 和 微队列(Microtask Queue) 中等待执行

事件循环流程

  • 先会执行主进程栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,然后在取宏任务清微任务这样不停的循环
while (true) {
  queue = getNextQueue(); // 如果是多队列情况则取一个队列
  task = queue.pop(); // 取第一个任务
  execute(task); // 执行任务

  // 如果有微任务那么执行所有的微任务
  while (microTasks.hasTasks()) {
    doMicroTask();
  }

  // 浏览器渲染阶段
  if (isRepaintTime()) {
    // 动画队列有任务 那么执行所有动画 task(requestAnimationFrame)
    animationTasks = animationQueue.copyTasks();
    for (task in animationTasks) {
      doAnimationTask(task);
    }
    // 绘制
    repaint();
  }
}
  • 当主线程结束,从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行
  • 执行完该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空
  • 当微任务队列清空后,一个事件循环结束
  • 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止
  • 当微任务队列处理完成并清空时,事件循环会检查是否需要更新 UI 渲染,如果是,则会重新渲染 UI 视图

单次循环迭代中,最多处理一个宏任务(其余的在队列中等待),而队列中的所有微任务都会被处理

为什么微任务执行更早

  • 微任务是语法规定的
  • 宏任务执行时间一般比较长
  • 宏任务是由浏览器规定的(web api)
  • 每一次宏任务开始之前一定是伴随着一次 event loop 结束的,而微任务是在一次 event loop 结束前执行的

页面渲染

  • 每次当一次事件循环结束后,即一个宏任务执行完成后以及微任务队列被清空后,浏览器就会进行一次页面更新渲染。通常我们浏览器页面刷新频率是 60fps,也就是意味着 16.67ms 要刷新一次,因此我们也要尽量保证一次事件循环控制在 16.67ms 之内,这也是我们需要做代码性能优化的一个原因。
  • requestAnimationFrame 在重新渲染屏幕之前执行,非常适合用来做动画
  • resize 和 scroll 事件其实自带节流,它只在 Event Loop 的渲染阶段去派发事件到 EventTarget 上

执行 setTimeout/setInterval 时发生了什么

  • JS 引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件, 而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中

执行 XHR/fetch 时发生了什么

  • JS 引擎线程通知异步 http 请求线程,发送一个网络请求,并制定请求完成后的回调事件, 而异步 http 请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中

参考