- 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()) {
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 上
- JS 引擎线程通知定时触发器线程,间隔一个时间后,会触发一个回调事件, 而定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列中
- JS 引擎线程通知异步 http 请求线程,发送一个网络请求,并制定请求完成后的回调事件, 而异步 http 请求线程在接收到这个消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中